GtkListView 入门

一些 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 模板指定为资源而不是直接嵌入它。

深入了解

我们今天在这里描述的所有内容也适用于网格视图,只需进行少量调整。

到目前为止,我们专注于视图方面。关于模型也有很多要说的。

然后是列视图,它值得单独写一篇文章。