GTK 4 中关于列表的更多信息

上一篇文章介绍了在 3.98.5 版本中引入的整体列表视图框架,并展示了一些示例。在这篇文章中,我们将深入探讨一些技术领域,并研究一些相关主题。

GtkTreeView 的一个重要功能是它可以显示树。毕竟,这就是它名字的由来。GtkListView 侧重于普通列表,并使处理这些列表更容易。特别是,GListModel API 比 GtkTreeModel 简单得多——这就是为什么自定义树模型实现相对较少的原因,但 GTK 4 已经有大量的列表模型。

但我们仍然需要显示树,有时是这样。这有一些复杂性,但我们已经找到了一种方法来实现它。

模型

我们需要的第一个要素是一个模型。由于 GListModel 表示项目的线性列表,我们必须更加努力地使其处理树。

GtkTreeListModel 通过一种按需创建新的子列表模型的方式来扩展项目来实现这一点。GtkTreeListModel 中的项目是 GtkTreeListRow 的实例,它们包装了模型中的实际项目,并且有一些函数,如 gtk_tree_list_row_get_children() 用于获取子模型的项目,以及 gtk_tree_list_row_get_item() 用于获取原始项目。GtkTreeListRow 具有一个 :expanded 属性,用于跟踪当前是否显示子项。

树列表模型的核心是 GtkTreeListModelCreateModelFunc,它接受列表中的一个项目,并返回一个新的列表模型,其中包含应为树中给定项目的子项的相同类型的项目。

这是一个 GSettings 对象的树列表模型的示例。该函数枚举给定 GSettings 对象的子设置,并为它们返回一个新的列表模型

static GListModel *
create_settings_model (gpointer item,
                       gpointer unused)
{
  GSettings *settings = item;
  char **schemas;
  GListStore *result;
  guint i;

  schemas = g_settings_list_children (settings);

  if (schemas == NULL || schemas[0] == NULL)
    {
      g_free (schemas);
      return NULL;
    }

  result = g_list_store_new (G_TYPE_SETTINGS);
  for (i = 0; schemas[i] != NULL; i++)
    {
      GSettings *child = g_settings_get_child (settings, schemas[i]);
      g_list_store_append (result, child);
      g_object_unref (child);
    }

  g_strfreev (schemas);

  return G_LIST_MODEL (result);
}

展开器

我们需要的下一个要素是一个小部件,用于显示用户可以单击以控制 :expanded 属性的展开箭头。这是由 GtkTreeExpander 小部件提供的。正如 GtkTreeListRow 项目包装模型中的基础项目一样,您可以使用 GtkTreeExpander 小部件来包装用于显示项目的小部件。

以下是树展开器在操作中的外观,用于我们的 GSettings 示例

完整的示例可以在此处找到。

排序

在离开树之前要讨论的最后一个主题是排序。列表通常有多种排序方式:a-z、z-a、忽略大小写等等。列视图通过允许您将排序器与列关联来实现这一点,用户可以通过单击列标题来激活它们。此 API 为

gtk_column_view_column_set_sorter (column, sorter)

在对树进行排序时,您通常希望排序顺序应用于树中给定级别的项目,而不是跨级别应用,因为这会打乱树结构。GTK 使用 GtkTreeListRowSorter 来支持这一点,它包装现有的排序器并使其遵守树结构

sorter = gtk_column_view_get_sorter (view);
tree_sorter = gtk_tree_list_row_sorter_new (sorter);
sort_model = gtk_sort_list_model_new (tree_model,           
                                      tree_sorter);
gtk_column_view_set_model (view, sort_model);

总之,树在新列表小部件中被稍微弱化了一些,它们为机制增加了很多复杂性,但它们在列表视图和列视图中都得到完全支持

组合框

使用单元格渲染器的更成问题的区域之一是我们的单选控件:GtkComboBox。这从来都不是一个很好的选择,特别是在与嵌套菜单结合使用时。因此,我们很想在新列表视图机制上尝试 GtkComboBox 的替代品。

从设计方面来说,长期以来也一直希望对组合框进行改进,正如在 2015 年的这个模拟图中看到的那样

五年后,我们终于有了一个替代小部件。它被称为 GtkDropDown。新小部件的 API 尽可能地简洁,几乎所有的工作都由列表模型和项目工厂机制完成。基本上,您使用 gtk_drop_down_new() 创建一个下拉列表,然后您为其提供一个工厂和一个模型,就可以了。

由于大多数选择都由简单的字符串组成,因此有一个便捷方法可以从字符串数组中为您创建模型和工厂

const char * const times[] = {
  "1 minute",
  "2 minutes",
  "5 minutes",
  "20 minutes",
  NULL
};

button = drop_down_new ();
gtk_drop_down_set_from_strings (GTK_DROP_DOWN (button), times);

这个便捷 API 与 GtkComboBoxText 非常相似,GtkBuilder 的支持也非常相似。您可以在 ui 文件中指定一个字符串列表,如下所示

<object class="GtkDropDown">
  <items>
    <item translatable="yes">Factory</item>
    <item translatable="yes">Home</item>
    <item translatable="yes">Subway</item>
  </items>
</object>

以下是一些正在运行的 GtkDropDown

摘要

再次强调这一点:所有这些都是全新的 API,我们很乐意听到您对哪些方面运行良好、哪些方面运行不佳以及缺少哪些方面的反馈。

GTK 4 中的可伸缩列表

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

历史:树视图和列表框

自古以来(即 GTK 2),GtkTreeView 一直是 GTK 中首选的数据显示小部件。它使用模型-视图模式将数据与显示基础结构分开。多年来,它已经发展出一个网格显示同级 (GtkIconView) 和一个选择表亲 (GtkComboBox),使用相同的基础结构(树模型和单元格渲染器)。

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

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

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

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

新基础设施

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

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

模型

对于模型-视图体系结构的模型方面,我们已经放弃了 GtkTreeModel,现在正在使用 GListModel。这样做有几个原因。其中之一是,我们希望项目是具有属性的对象,因此我们可以使用属性绑定。另一个是 GtkTreeModel 本身的可伸缩性并不高(例如,参见此案例,该案例描述了无意中出现的二次行为)。

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

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

传统上返回各种项目 GList 的 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 的目标。