一些 GTK4 的早期采用者指出,新的列表小部件并不容易学习。特别是,GtkExpression 和 GtkBuilderListItemFactory 很难理解。这并不太令人惊讶 - 一个完整的列表小部件,带有列、选择、排序和树结构等,是一个复杂的野兽。
但是,让我们看看是否可以逐一解开这些东西,并使其更容易理解。
概述
让我们从相关组件及其交互的高级视图开始:模型、列表项工厂和列表视图。
它们是我们在创建列表视图时发生的三件事
view = gtk_list_view_new (model, factory);
我们使用的模型是 GListModel。这些模型始终包含 GObject,因此你必须以对象的形式提供数据。这是与 GtkTreeview 的第一个显着区别,后者使用直接包含基本类型的 GtkTreeModel。
对于某些简单的情况,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 作为参数,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” 呢?在上面的示例中,绑定回调获取项的属性 (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 模板指定为资源而不是直接嵌入它。
深入研究
我们今天在这里描述的所有内容也适用于网格视图,只需进行少量调整即可。
到目前为止,我们一直专注于视图方面。关于模型也有很多要说的。
然后是列视图,它值得单独发布一篇博文。