GTK 4 中的可伸缩列表

GTK 4 中最后缺失的重要部分之一是新的列表和网格部件的基础设施。它刚刚被合并,并包含在 3.98.5 版本中。因此,现在是时候仔细看看了。

历史:树形视图和列表框

自古以来(即 GTK 2),GtkTreeView 一直是 GTK 中首选的数据显示部件。它使用模型-视图模式将数据与显示基础设施分离。多年来,它衍生出了一个网格显示兄弟(GtkIconView)和一个选择表亲(GtkComboBox),使用相同的基础设施(树模型和单元格渲染器)。

不幸的是,GtkTreeView 中使用单元格渲染器进行渲染的方法给我们留下了一个分裂:部件使用一组 vfunc 和技术进行大小分配和渲染,而单元格渲染器使用另一组。这种分裂的一个不幸后果是,很难在树形视图中进行动画(因为单元格渲染器不保留状态)。另一个后果是,GTK CSS 渲染机制的大部分进展在单元格渲染器中都不可用。

因此,我们真的希望使用部件来显示列表中的数据。在 GTK 3 时代,我们为此目的引入了许多容器:GtkListBox 用于列表,GtkFlowBox 用于网格。它们不使用单元格渲染器,因此上述限制不是问题。它们甚至可以使用列表模型来保存数据。但是它们为每个数据项生成一个部件,这严重限制了它们的可伸缩性。根据经验,GtkListBox 可以很好地处理 1000 个项目,而 GtkTreeView 可以很好地处理 100000 个项目。

克服可伸缩性限制,同时仍然使用部件进行所有渲染,长期以来一直在我们的路线图上。

可伸缩性限制
部件 可伸缩性
GtkIconView 100
GtkListBox 1 000
GtkTreeView 100 000
GtkListView 无限制

新的基础设施

使用列表视图系列部件,我们希望最终实现这一目标。目标是很好地处理无限数量的项目。如果您使用过 Android 回收器视图之类的东西,您会认识到列表视图背后的基本思想

  • 项目的数据以模型(包含对象)的形式提供
  • 仅为项目的可见范围创建部件
  • 部件可以通过将它们绑定到不同的项目来回收利用
  • 尽可能避免迭代模型中的所有项目,而只处理绑定到部件的可见范围内的项目

模型

对于我们模型-视图架构的模型端,我们已经从 GtkTreeModel 转移,现在使用 GListModel。 这有几个原因。其中之一是我们希望项目成为具有属性的对象,因此我们可以使用属性绑定。另一个原因是 GtkTreeModel 本身的可伸缩性并不强(例如,参见这个 案例,它是不经意的二次行为)。

GTK 4 附带了丰富的 GListModel 实现,从组合或修改现有模型的各种方法到过滤和排序。 几个 GtkFilter 和 GtkSorter 类支持过滤和排序。 选择也在模型端处理,使用 GtkSelectionModel 及其子类。

最后,还有用于我们正在处理的各种类型数据的具体模型:用于文件的 GtkDirectoryList,用于字体的 PangoFontMap。

传统上返回各种项目 GLists 的 API 已被更改或补充了返回 GListModel 的 API,例如 gdk_display_get_monitors()gtk_window_get_toplevels()gtk_stack_get_pages()

工厂

既然我们谈论的是按需自动创建部件,那么就会涉及到工厂。 GtkListItemFactory 是负责为模型中的项目创建部件的对象。

这个工厂有不同的实现。其中一种是 GtkBuilderListItemFactory,它使用 ui 文件作为列表项部件的模板。这是一个典型的例子

<interface>
  <template class="GtkListItem">
    <property name="child">
      <object class="GtkLabel">
        <property name="xalign">0</property>
        <property name="wrap">1</property>
        <property name="width-chars">50</property>
        <property name="max-width-chars">50</property>
        <binding name="label">
          <lookup name="summary" type="SettingsKey">
            <lookup name="item">GtkListItem</lookup>
          </lookup>
        </binding>
      </object>
    </property>
  </template>
</interface>

完整的示例可以在 gtk4-demo 中找到。

另一个列表项工厂实现 GtkSignalListItemFactory 接受回调来设置拆除绑定解除绑定来自项目的部件。

static void
setup_listitem_cb (GtkListItemFactory *factory,
                   GtkListItem        *list_item)
{
  GtkWidget *image;

  image = gtk_image_new ();
  gtk_image_set_icon_size (GTK_IMAGE (image), GTK_ICON_SIZE_LARGE);
  gtk_list_item_set_child (list_item, image);
}

static void
bind_listitem_cb (GtkListItemFactory *factory,
                  GtkListItem *list_item)
{
  GtkWidget *image;
  GAppInfo *app_info;

  image = gtk_list_item_get_child (list_item);
  app_info = gtk_list_item_get_item (list_item);
  gtk_image_set_from_gicon (GTK_IMAGE (image),
                            g_app_info_get_icon (app_info));
}

static void
activate_cb (GtkListView  *list,
             guint         position,
             gpointer      unused)
{
  GListModel *model;
  GAppInfo *app_info;

  model = gtk_list_view_get_model (list);
  app_info = g_list_model_get_item (model, position);
  g_app_info_launch (app_info, NULL, NULL, NULL);
  g_object_unref (app_info);
}

...

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);

list = gtk_list_view_new_with_factory (factory);
g_signal_connect (list, "activate", activate_cb, NULL);

model = create_application_list ();
gtk_list_view_set_model (GTK_LIST_VIEW (list), model);
g_object_unref (model);

gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (sw), list);

完整的示例可以在这里找到。

表达式

为了将工厂创建的部件绑定到项目中的数据,我们需要一个灵活的机制来绑定属性。GObject 的 GBinding 机制朝着正确的方向发展,但不够灵活,无法处理您可能需要绑定子对象或部件(位于部件层次结构的深处)的属性,以及这些对象在设置绑定时甚至可能不存在的情况。

为了处理这个问题,我们引入了 GtkExpression 作为一种更灵活的绑定系统,可以表达如下内容

label = this->item->value

其中 this 是一个 GtkListItem,它有一个 item 属性(类型为 SettingsKey),我们希望将其 value 属性绑定到 label 属性。在 GtkBuilder ui 文件中表达相同的内容看起来有点笨拙

<binding name="label">
  <lookup name="value" type="SettingsKey">
    <lookup name="item">GtkListItem</lookup>
  </lookup>
</binding>

新的部件

GtkListView 是一个简单的列表,没有列或标题。使用此类列表的一个示例是 GtkFontChooser。GtkListView 开创新局面的一点是,它可以设置为水平列表,以及通常的垂直方向。

GtkGridView 将部件放置在一个可以重新排列的网格中,很像 GtkFlowBox 或 GtkIconView。

GtkColumnView 相当于完整的 GtkTreeView,具有多个列和标题,以及交互式调整大小和重新排序等功能。就像 GtkTreeView 有 GtkTreeViewColumns 一样,GtkColumnView 有一个 GtkColumnViewColumns 列表。每一列都有一个工厂,为每个项目生成一个单元格。然后将这些单元格组合成该项目的行。

示例

复杂 GTK 对话框中的许多列表(尽管还不是全部)已被新的列表部件取代。例如,字体选择器现在使用 GtkListView,GTK 检查器中的大多数列表都使用 GtkColumnView。

但是 gtk4-demo 包含许多新部件的示例。这里有一些

时钟示例展示了具有部件渲染的完全灵活性的优点。

颜色示例以各种方式显示了中等大小的数据集。

设置示例表明,列视图在功能方面或多或少与 GtkTreeView 相匹配。

总结

这篇文章介绍了新的列表部件。我们这里没有涉及到更多内容,例如树或组合框替换。要了解有关新 API 的更多信息,请访问 GTK 文档中的 详细介绍

我们现在已经将列表视图基础设施合并到主分支。这并不意味着它已完成。但我们认为它已准备好进行更广泛的使用,我们希望收到您的反馈,了解哪些工作正常,哪些工作不正常,以及缺少什么。

而且,要明确的是,这并不意味着我们正在从 GTK 4 中删除树形视图和组合框——现在为时已晚,它们仍然在 GTK 内部的许多地方使用。这可能是 GTK 5 的目标。