一些 GTK4 的早期采用者指出,新的列表小部件并非最容易学习的。特别是,GtkExpression 和 GtkBuilderListItemFactory 很难理解。这并不奇怪——一个完整的列表小部件,带有列、选择和排序以及树结构等,是一个复杂的野兽。
但是,让我们看看是否可以逐一解开这些东西,并使其更易于理解。
概述
让我们从相关组件及其交互的高级视图开始:模型、列表项工厂和列表视图。
它们是创建列表视图时发生的三件事
view = gtk_list_view_new (model, factory);
我们使用的模型是 GListModels。这些总是包含 GObject,因此您必须以对象的形式提供数据。这是与 GtkTreeview 的第一个显著区别,后者使用直接包含基本类型的 GtkTreeModels。
对于某些简单的情况,GTK 提供了现成的模型,例如 GtkStringList。但通常,您必须制作自己的模型。值得庆幸的是,GListModel 是一个比 GtkTreeModel 简单得多的接口,因此这并不太难。

列表项工厂的职责是生成一个行小部件,并在列表视图需要时将其连接到模型中的项目。
列表视图将创建比填充其可见区域所需的多一些行,以更好地估计滚动条的大小,并为了在您决定滚动视图时有一些“缓冲”。
一旦您进行滚动,我们不一定需要要求工厂制作更多行——我们可以回收正在另一端滚动出视图的行。

值得庆幸的是,所有这些都在幕后自动发生。您要做的就是提供一个列表项工厂。
创建项目
GTK 提供了两种不同的创建项目的方法。您可以手动使用 GtkSignalListItemFactory,也可以使用 GtkBuilderListItemFactory 从 ui 文件实例化行小部件。
手动方法更容易理解,所以让我们先看看这个。
factory = gtk_signal_list_item_factory_new ();
g_signal_connect (factory, "setup", setup_listitem_cb, NULL);
g_signal_connect (factory, "bind", bind_listitem_cb, NULL);
当工厂需要创建新的行小部件时会发出“setup”信号,当需要将行小部件连接到模型中的项目时会发出“bind”信号。
这两个信号都以 GtkListItem 作为参数,这是一个包装对象,可让您获取模型项(使用 gtk_list_item_get_item())并允许您传递新的行小部件(使用 gtk_list_item_set_child())。
static void
setup_listitem_cb (GtkListItemFactory *factory,
GtkListItem *list_item)
{
GtkWidget *label = gtk_label_new ("");
gtk_list_item_set_child (list_item, label);
}
通常,您的行会比单个标签更复杂。您可以根据需要创建复杂的小部件并将它们分组在容器中。
static void
bind_listitem_cb (GtkListItemFactory *factory,
GtkListItem *list_item)
{
GtkWidget *label;
MyObject *obj;
label = gtk_list_item_get_child (list_item);
obj = gtk_list_item_get_item (list_item);
gtk_label_set_label (GTK_LABEL (label),
my_object_get_string (obj));
}
如果您的“bind”处理程序连接到项目上的信号或执行其他需要清理的操作,则可以使用“unbind”信号来执行该清理操作。“setup”信号有一个类似的对应项,称为“teardown”。
构建器方式
我们的“setup”处理程序基本上是创建小型小部件层次结构的配方。GTK 有一种更声明式的方法来执行此操作:GtkBuilder ui 文件。这就是 GtkBuilderListItemFactory 的工作方式:您给它一个 ui 文件,它会在需要创建行时实例化该 ui 文件。
ui = "<interface><template class="GtkListItem">...";
bytes = g_bytes_new_static (ui, strlen (ui));
gtk_builder_list_item_factory_new_from_bytes (scope, bytes);
您现在可能会想:等一下,您正在为我模型中的数千个项目中的每个项目解析一个 xml 文件,这不是很昂贵吗?
对此问题有两个答案
- 我们并不是真的为每个条目都解析 XML;我们只解析一次,存储回调序列,然后在之后重放它。
- GtkBuilder 最耗时的部分实际上不是 XML 解析,而是对象的创建;回收行有助于解决这个问题。
很容易理解 UI 文件如何替换“setup”处理程序,但是“bind”呢?在上面的例子中,“bind”回调函数获取条目的属性(MyObject:string 属性),并使用它们的值来设置小部件的属性(GtkLabel:label 属性)。换句话说,“bind”处理程序正在执行属性绑定。为了简单起见,我们在这里只创建了一个一次性绑定,但是我们也可以使用 g_object_bind_property() 来创建一个持久绑定。
GtkBuilder UI 文件可以设置对象之间的属性绑定,但存在一个问题:模型项在 UI 文件中不是“存在的”,它只是在稍后的“bind”时间才与行小部件相关联。
这就是 GtkExpression 发挥作用的地方。GtkExpression 的核心是一种描述对象之间绑定的方式,而这些对象不一定已经存在。在我们的例子中,我们想要实现的是
label->label = list_item->item->string
不幸的是,当它作为我们 UI 文件的一部分转换为 XML 时,会变得稍微笨拙一些
<interface>
<template class="GtkListItem">
<property name="child">
<object class="GtkLabel">
<binding name="label">
<lookup name="string">
<lookup name="item">GtkListItem</lookup>
</lookup>
</binding>
</object>
</property>
</template>
</interface>
请记住,UI 模板中的类名 (GtkListItem) 用作指向正在实例化的对象的 “this” 指针。
因此,<lookup name="item">GtkListItem</lookup> 的意思是:创建的列表项的 “item” 属性的值。 <lookup name="string"> 的意思是:该对象的 “string” 属性。 <binding name="label"> 表示将小部件的 “label” 属性设置为该属性的值。
由于表达式的工作方式,当列表项工厂将列表项的 “item” 属性设置为新值时,所有这些都将被重新评估,这正是我们使行小部件回收工作所需的。
专家级迷惑
当您嵌套事物时,GtkBuilderListItemFactory 和 GtkExpression 可能会变得非常令人困惑——列表项工厂可以在 UI 文件本身中构建,并且它们可以将自己的 UI 文件作为属性赋予,因此您最终会得到如下结构:
<object class="GtkListView">
<property name="factory">
<object class="GtkBuilderListItemFactory">
<property name="bytes"><![CDATA[
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="GtkListItem">
<property name="child">
...
]]>
</property>
</object>
</property>
...
即使对于 GTK 专家来说,这也很令人困惑。
我的建议是,在刚开始使用 GtkListView 时避免这种情况——您不必在 UI 文件中创建列表项工厂,并且可以将它的 UI 模板指定为资源,而不是直接嵌入它。
深入了解
我们今天在这里描述的所有内容也适用于网格视图,只需进行少量调整。
到目前为止,我们专注于视图方面。关于模型也有很多要说的。
然后是列视图,它值得单独写一篇文章。