今天我们将邀请客座作者 Timm Bäder,Corebird 的维护者和 GTK+ 的贡献者,谈谈 GTK+ 4.0 中编写复合窗口小部件的更改。
(注意:这里的一些信息基于尚未合并到主分支的分支,但我相信它们将在不久的将来合并)
在 GTK+3 中,只有 GtkContainer 子类可以拥有子窗口小部件。对于我们所知的“公共”容器子项来说,这很有意义,例如 GtkBox — 即开发人员可以任意添加、删除和重新排序子窗口小部件,而容器只需进行布局即可。
但是,GTK+3 中还有一些更复杂的窗口小部件不继承自 GtkContainer,例如 GtkSpinButton 或 GtkSwitch。这些从来没有真正的 GtkWidget 子项。例如,考虑 GtkSpinButton 中的两个可点击区域。我在这里没有称它们为“按钮”是有原因的,因为在 GTK+3 中,它们不是实际的 GtkButton 实例,因为 GtkSpinButton 不是 GtkContainer。相反,GtkSpinButton 必须解决这个问题,并为向上/向下区域创建两个 GdkWindow,然后在其中渲染两个图标;关心悬停和 CSS 状态;各种按钮的向上/向下事件;以及 GdkWindow 的生命周期等。为了解决 GtkContainer 的要求,我们在 GTK+3 中引入了小工具 (GtkCssGadget)。在样式方面,小工具对应于一个 CSS 框,因此代表 CSS 树中的一个节点。在窗口小部件方面,它们被用于使非容器窗口小部件拥有“类似窗口小部件”、可使用 CSS 设置样式的子项。
GtkWidget 的更改
当然,在 GTK+4 中,需要进行大量的更改才能支持这些用例。我不会在这里列出所有更改(特别是那些更难看的更改,例如焦点处理),但我认为其中大多数对于应用程序开发人员和自定义窗口小部件作者来说都非常有趣和重要。总的来说,我们正在努力摆脱特殊情况,并通过尽可能重复使用窗口小部件来采用更通用的方式。因此,窗口小部件应该使用 GtkLabel,而不是使用 PangoLayout 来显示文本。如果你有一个具有按钮式语义的可点击区域,请尝试使用 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() 调用的名称。如果未使用上述 2 个中的任何一个,则 CSS 节点将简单地称为“widget”。
已转换的窗口小部件示例
当前主分支(以及侧分支)中已经有一些小部件从使用各种 GtkCssGadget、GdkWindow 和 PangoLayout 转换为使用实际的小部件。最终目标当然是尽可能重用小部件,从而减少代码量和维护负担。
GtkSwitch
在 GTK+3 中,GtkSwitch 是 GtkWidget 的直接子类(好耶!),它使用一个 GdkWindow 进行输入(点击开关将启用/禁用它),一个 GtkCssGadget 用于小部件本身,两个 PangoLayout 用于显示 ON/OFF 文本,以及另一个 GtkCssGadget 用于滑块。

在 GTK+ 主分支中,开关仍然有其小部件级别的
GtkCssGadget,因此它支持 min-width/min-height CSS 属性和 CSS 外边距,但滑块 gadget 已被
GtkButton 替换,并且两个
PangoLayout 被
GtkLabel 替换。这样,我们可以在
gtkswitch.c 中节省大约 300 行代码。
理论上,我们还拥有更多功能,例如可以使用
GtkLabel 支持的有限的 text-decoration CSS 属性,但我只是怀疑这是否非常有用。
GtkSpinButton

如前所述,
GtkSpinButton 可以很容易地使用实际的
GtkButton 作为向上/向下区域,并且它在 GTK+ 主分支中这样做(在某个时候将成为 GTK+4)。这消除了
gtkspinbutton.c 中的另外 300 行代码。通过使用
GtkButton,旧的图标辅助 gadget 也变成了实际的
GtkImage 实例。不幸的是,我们必须在这里自己实现一些
GtkGesture 的魔力,因为
GtkSpinButton 还支持对其按钮进行鼠标中键和右键单击,而
GtkButton::clicked 仅对单个主鼠标按钮单击做出反应。
GtkLevelBar
GtkLevelBar 管理一组块并为它们分配不同的样式类。在 GTK+3 中,这些块都是 GtkCssGadget 实例。它们都是“哑的”,因为它们不做任何特殊的事情 —— 它们只是 CSS 盒子而已。这就是为什么将其转换为对所有块使用 GtkWidget 没有获得太多文件大小的减少的原因。
GtkProgressBar
GtkProgressBar 使用 gadget 来表示槽和进度节点。它还使用 PangoLayout 来显示百分比或用户给定的字符串。
在主分支中,槽和进度都是小部件,并且 PangoLayout 当然是一个 GtkLabel。不必监听 GtkWidget::style-changed(这对于小部件是自动完成的),也不必自己绘制 PangoLayout(现在由 GtkLabel 处理),这样可以节省大约 200 行代码,这还是很不错的。
GtkExpander
GtkExpander 比它看起来更复杂。在 GTK+3 中,它由 2 个 GtkBoxGadget(类似于 GtkBox,但不是小部件……)、一个用于标题小部件左侧箭头的 gadget、标题小部件和实际内容小部件组成。在主分支中,这是使用实际的 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 中删除子小部件会发生什么?如果您设置 GtkFontButton 的 GtkButton:label 属性会发生什么?同样,这些都是封闭的实体,它们具有自己的 API 来设置和获取各种数据,并根据它们更改行为和/或外观,但这并不意味着它们支持所有 GtkButton/GtkContainer 的恶作剧。
通用重构规则和未来
对于这项重构工作,我们尝试保持 CSS 节点结构与 GTK+3 中的结构相同,即我们尝试不破坏我们当前在 testsuite/css/nodes.c 中拥有的 CSS 节点测试。
GTK+ 中一些更复杂的小部件仍然严重依赖 gadget,将它们移植为仅使用实际小部件将需要大量工作。GtkRange 从历史上看是 GTK+ 中最复杂的非容器小部件之一。它既用于滚动条又用于刻度,因此将其移植到小部件可能首先需要进行另一轮重构。
另一个有趣的例子是 GtkNotebook,它结合了 gadget 和小部件的使用。在这里,例如,我们可以使用真正的 GtkStack 来在页面之间切换,并轻松支持页面切换过渡。