一些 GTK4 的早期采用者指出,新的列表控件不太容易学习。特别是,GtkExpression 和 GtkBuilderListItemFactory 很难理解。这并不令人意外 – 一个完整的列表控件,带有列、选择、排序和树结构等等,是一个复杂的实体。
但让我们看看是否可以逐一拆解,使其更易于理解。
概述
让我们从相关组件及其交互的高级视图开始:模型、列表项工厂和列表视图。
它们是创建列表视图时出现的三个要素。
view = gtk_list_view_new (model, factory);
我们使用的模型是 GListModels。这些模型始终包含 GObjects,因此您必须以对象的形式提供数据。这是与 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”。
使用 Builder 的方式
我们的“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 模板指定为资源而不是直接嵌入它。
深入了解
我们今天在这里描述的所有内容也适用于网格视图,只需进行少量调整。
到目前为止,我们专注于视图方面。关于模型也有很多要说的。
然后是列视图,它值得单独写一篇文章。