GtkColumnView

在我最近关于 GTK 4 中的列表视图模型的系列文章中,我留下了一个未完成的部分,那就是详细介绍 GtkColumnView。这很可能是本系列中最复杂的部分。我们正在进入 GtkTreeView 的核心地带——任何旨在取代其大部分功能的组件都将是一个复杂的怪兽。

概述

就像我们对 GtkListView 所做的那样,我们将从一个高层次的概述和一个图片开始。

如果你回顾一下listview的图片,你会记得我们使用列表项工厂为模型中每个需要显示的项创建一个小部件。

在列视图中,我们需要为每个项提供多个小部件——每列一个。我们通过为每列提供自己的列表项工厂来实现这一点。每当我们需要显示一个新项时,我们会将来自每列工厂的小部件组合成新项的一行。

在内部,列视图实际上是使用列表视图来保存行的。这很好,因为我之前关于项重用以及如何使用列表项工厂的文章中解释的所有内容都同样适用。

当然,有些事情是不同的。例如,列视图必须组织大小分配,以便所有行中的小部件对齐形成正确的列。

注意:就像 GtkListView 一样,列视图只为当前在视图中的模型片段创建小部件,因此它具有垂直可伸缩性。但水平方向并非如此——每一行都完全填充了每个列的小部件,即使它们在左侧或右侧的视图之外。因此,如果添加大量列,速度会变慢。

标题和其他复杂情况

列对象还包含其他数据,例如标题。列视图使用这些数据来显示每列的标题。如果列视图被标记为可重新排序,则可以通过拖放标题小部件来重新排列列。如果列被标记为可调整大小,则可以拖动两列之间的边界来调整它们的大小。

如果你注意了,你现在可能会想,这种调整大小是如何与行中的单元格可以是任意小部件(期望至少有其最小尺寸可用于绘制其内容)的事实结合在一起的。答案是,我们正在使用 GTK 4 渲染机制的另一个新功能:小部件可以控制如何处理其边界之外(由子小部件)的绘制,使用

 gtk_widget_set_overflow (cell, GTK_OVERFLOW_HIDDEN)

排序、选择和对 treeview 对等性的追求

由于我们希望在功能上与 GtkTreeview 相匹配,所以我们还没有完成。用户喜欢在树视图中做的另一件事是点击标题,以按该列对内容进行排序。GtkColumnView 标题也允许这样做。

你可能还记得上一篇文章中,排序是通过将数据包装在 GtkSortListModel 中,并为其提供合适的排序器对象来完成的。由于我们希望根据你点击的列标题具有不同的排序顺序,因此我们为每列提供自己的排序器,你可以使用以下方法设置:

gtk_column_view_column_set_sorter (column, sorter)

但是,我们如何从你刚刚点击的列中获取正确的排序器,并将其附加到排序模型?请记住,排序模型不是我们要传递给列视图的最外层模型,因为它始终是一个选择模型,因此列视图无法自行切换排序列表模型上的排序器。

我们想出的解决方案是让列视图提供一个排序器,该排序器在内部使用列排序器,使用

gtk_column_view_get_sorter (view)

你可以在设置模型时,将此排序器一次性提供给你的排序模型,然后当用户点击列标题以激活不同的列排序器时,事情将自动更新。

这听起来很复杂,但它出奇地好用。这种方法的一个好处是,我们实际上可以一次按多列排序——因为我们拥有所有可用的列排序器,并且我们知道你最后点击了哪一个。

相比之下,选择处理很容易。它的工作方式与 GtkListView 中的相同。

总结

GtkColumnView 是一个复杂的小部件,但我希望这一系列文章能让你更容易开始使用它。

关于列表模型

在之前的文章中,我承诺深入研究列表模型以及 GTK 4 在这方面提供的功能。让我们首先看看GListModel接口

struct _GListModelInterface
{
  GTypeInterface g_iface;

  GType    (* get_item_type) (GListModel *list);
  guint    (* get_n_items)   (GListModel *list);
  gpointer (* get_item)      (GListModel *list,
                              guint       position);
};

实现该接口的一个重要部分是,你需要发出
::items-changed 信号,在需要时,使用辅助函数
GLib 为此目的提供的

void g_list_model_items_changed (GListModel *list,
                                 guint       position,
                                 guint       removed,
                                 guint       added)

关于这个接口,需要注意以下几点

  • 非常简洁;这使得它易于实现
  • API 基于位置,并且只处理列表成员的变化——跟踪项目本身的更改由你自己负责

列表模型动物园

GTK 提供大量列表模型实现。经过仔细检查,它们分为几个不同的组。

列表模型构建工具包

第一组可以称为列表模型构建工具包:允许你通过修改或组合你已经拥有的模型来构建新模型的模型。

该组中的第一个模型,GtkSliceListModel,获取现有模型的切片(由偏移量和大小给出),并创建一个仅包含这些项的新模型。如果你想在分页视图中呈现一个大列表,这将非常有用——向前和向后按钮只会将偏移量增加或减少该大小。切片模型还可以用于通过随着时间的推移增大切片来增量填充列表。GTK 在某些地方正在使用这种技术。

该组中的下一个模型,GtkFlattenListModel,获取多个列表模型并将它们合并为一个。由于这完全是关于列表模型的,要组合的模型以列表模型的列表的形式传递给扁平模型。每当你需要合并来自多个来源的数据时,这将非常有用,例如 GTK 对打印对话框中的纸张尺寸的处理。

Paper size list in print dialog
一个扁平化的列表

请注意,原始模型继续存在于扁平模型之后,它们的更新将按照预期由扁平列表模型传播。

有时,你的数据在列表模型中,但它的形式不太正确。在这种情况下,你可以使用 GtkMapListModel 将原始模型中的每个项替换为不同的项。

具体模型

GTK 及其依赖项包含许多用于我们自己处理的数据类型的具体模型。

这里的第一个例子是实现列表模型接口的 Pango 对象:PangoFontMap 是 PangoFontFamily 对象的列表模型,而 PangoFontFamily 是 PangoFontFace 对象的列表模型。字体选择器正在使用这些模型。

font chooser dialog
一个 Pango 列表模型

下一个例子是 GtkDirectoryListGtkBookmarkList 对象,它们将在文件选择器中使用来表示目录内容和书签。关于这些对象的一个有趣细节是,它们都需要执行 IO 来填充其内容,并且它们会异步执行此操作以避免长时间阻塞 UI。

该组中的最后一个模型稍微不那么具体:GtkStringList 是围绕非常常见的字符串数组的简单列表模型包装器。经常使用这种列表模型的示例是 GtkDropDown。这非常常见,以至于 GtkDropDown 有一个便捷的构造函数,它接受一个字符串数组并为你创建 GtkStringList

GtkWidget *
    gtk_drop_down_new_from_strings (const char * const * strings)

选择

下一组模型使用新的接口扩展了 GListModel:GtkSelectionModel。对于底层模型中的每个项,GtkSelectionModel 维护有关其是否被选中的信息。

我们不会详细讨论该接口,因为你不太可能需要自己实现它,但最重要的几点是

gboolean gtk_selection_model_is_selected (GtkSelectionModel *model)
                                          guint              pos)
GtkBitset *
       gtk_selection_model_get_selection (GtkSelectionModel *model)

因此,你可以获得单个项的选择信息,或者以位集的形式整体获取。当然,还有一个 ::selection-changed 信号,其工作方式与 GListModel 的 ::items-changed 信号非常相似。

GTK 有三种 GtkSelectionModel 实现:GtkSingleSelectionGtkMultiSelectionGtkNoSelection,它们的不同之处在于可以同时选择的项数(1、多个或 0)。

GtkGridView 颜色演示展示了一个带有橡皮筋的多选操作

 

在使用 GTK 的新列表小部件时,你很可能会遇到选择模型,因为它们都希望其模型是选择模型。

大模型

我想提及的最后一组模型是执行你在列表中期望的典型操作的模型:过滤和排序。这些模型是 GtkFilterListModelGtkSortListModel。它们都使用辅助对象来实现其操作:GtkFilter 和 GtkSorter。两者都有子类来处理常见情况:排序和过滤字符串或数字,或者使用回调。

在 GTK 3.99 的准备过程中,我们在这两个模型上花费了相当大的精力,并使它们以增量方式工作,以避免在处理大型模型时长时间阻塞 UI。

GtkListView 单词演示展示了对 500,000 个单词的列表进行交互式过滤

剩余部分

GTK 中还有一些列表模型实现不完全适合上述任何组,例如 GtkTreeListModelGtkSelectionFilterModelGtkShortcutController。今天我将跳过这些。

到处都是模型

最后,我将简要列出返回列表模型的 GTK API

  • gdk_display_get_monitors
  • gtk_widget_observe_children
  • gtk_widget_observe_controllers
  • gtk_constraint_layout_observe_constraints
  • gtk_constraint_layout_observe_guides
  • gtk_file_chooser_get_files
  • gtk_drop_down_get_model
  • gtk_list_view_get_model
  • gtk_grid_view_get_model
  • gtk_column_view_get_model
  • gtk_column_view_get_columns
  • gtk_window_get_toplevels
  • gtk_assistant_get_pages
  • gtk_stack_get_pages
  • gtk_notebook_get_pages

总而言之,列表模型在 GTK 4 中无处不在。它们灵活且有趣,你应该使用它们!

GtkListView 入门

一些 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 作为参数,它是一个包装对象,可以让您访问模型项(使用 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”呢?在上面的示例中,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 模板指定为资源而不是直接嵌入它。

深入了解

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

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

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

GTK 3.99.1

自从我们发布 GTK 3.99 以来已经过去一个月了,是时候发布另一个快照了。在这里:https://download.gnome.org/sources/gtk/3.99/gtk-3.99.1.tar.xz

此快照侧重于润色和完成。

未完成的工作

我们已经完成了 API 中的一些未完成的工作。

最明显的变化可能是按钮类层次结构的简化。GtkCheckButton 不再从 GtkToggleButton 派生,它们现在是两个独立的部件,并且它们都可以分组以互斥(也称为“单选组”)。在这种新的设置中,GtkRadioButton 实际上不再需要,因此被删除。

我们新的列表部件(GtkListView 和 GtkGridView)的 API 也进行了一些小的调整。我们现在明确要求模型属于 GtkSelectionModel 类型,以明确部件处理选择。我们已经去除了额外的“with_factory”构造函数,只在 new() 中采用可为空的工厂参数,这给我们留下了

GtkWidget * gtk_list_view_new (GtkSelectionModel  *model,
                               GtkListItemFactory *factory);

作为 API 清理,我们删除了所有 CSS 样式类的定义——我们的部件支持哪些样式类在其文档中定义,并且这些额外的定义实际上没有明确定义或有用。

我们的主题现在正在对 GtkFrame 部件绘制的框架的角进行圆角处理。这需要我们裁剪框架的子项——实际上不是 API 更改,而是一种值得一提的行为更改。

更多演示

过去一个月,我们为 gtk4-demo 投入了大量精力。

我们已经使源代码突出显示现代化。我们现在使用 highlight 命令行实用程序。除此之外,这使我们能够对 xml 和 css 进行语法突出显示,以及支持深色主题。

Highlighting XML in a dark theme
突出显示

演示列表具有更好的过滤和更好的外观。新外观是 Adwaita 现在支持的几个预定义列表样式之一:富列表、导航边栏和数据表。

 

Rich List list style
富列表
Navigation Sidebar list style
导航边栏
Data Table list style
数据表

我们已经从 gtk4-demo 中删除了一些过时的演示,并润色了许多现有的演示。这是我们现在的拖放演示的外观

Drag-and-Drop demo
拖放演示

还添加了许多新的演示。这是用于布局管理器和转换的新演示

性能和其他错误

许多错误已被修复;感谢我们热情的测试人员和错误报告人员。

最近,我们终于追踪到一个长期存在的问题,该问题导致我们的 GL 渲染器在存在非平凡的投影变换时裁剪错误。现在这个问题已得到纠正(结果可以在上面的转换演示中看到)。

作为之前提到过的突出显示改进的一部分,gtk_text_view_buffer_insert_markup() 的速度快了许多。这个改进之所以能够实现,仅仅是因为突出显示实用程序可以生成 Pango 标记。感谢实现这一功能的人!

我们解决的另一个性能问题是,在拥有大量字体的系统上,字体选择器对话框的加载时间。我们现在正在逐步填充字体列表。除了这个更改之外,调查还促成了 fontconfig 和 Pango 的性能改进,这将使这些库的任何用户受益。

我现在可以移植了吗?

答案是:可以!

现在是查看 GTK4、开始移植你的应用程序并向我们提供关于新旧 API 的反馈的好时机。我们也渴望看到你对于以意想不到的方式使用 GTK4 有什么想法——上面展示的一些演示可能会给你一些启发。

下一步是什么?

我们正计划尽快为新的可访问性接口添加 at-spi 后端;它应该会包含在下一个 GTK 快照中。

GTK 3.99

本周,我们发布了 GTK 3.99,这只能意味着一件事:GTK4 真的非常接近了!

早在 二月,当 3.98 发布时,我们概述了在发布功能完整的 3.99 版本之前想要实现的功能。这是列表

  • 键盘快捷键的事件控制器
  • 可移动的弹出窗口
  • 行回收列表和网格视图
  • 改进的可访问性基础架构
  • 动画 API
我们做得怎么样?

我们已从 4.0 阻止程序列表中删除了动画 API,因为它需要更广泛的内部重组,而我们无法及时完成。但是所有其他功能都已经进入了各种 3.98.x 快照中,可访问性基础结构是最近才最终完成的。

其中一些功能已经在本文中介绍过,例如 可移动的弹出窗口可伸缩列表。其他功能希望将来能在此处得到详细的审查。在那之前,如果你对新的可访问性基础结构感到好奇,可以看看 Emmanuele 在 GUADEC 上的 演讲

还有什么新内容?

我想强调的一个领域是,为充实新的可伸缩列表基础结构所做的工作量。我们的过滤器和排序模型现在是增量工作的,因此当在后台过滤或排序大型列表时,UI 可以保持响应。

一个新的 macOS GDK 后端已合并。它仍然有一些粗糙的地方,我们希望在现在到 4.0 版本发布之间将其平滑。

许多小的回归问题已得到修复,从微调按钮大小到树状视图单元格编辑,再到自动滚动、检查器导航和稍微渲染错误的阴影。

我现在可以移植了吗?

GTK 3.99 是首次查看移植应用程序的绝佳时机。

我们非常感谢那些使用试验性移植勇敢地尝试 3.96 或 3.98 快照并向我们提供宝贵反馈的早期采用者。由于进行了如此多的更改,我们不可避免地在 API 中犯了一些错误,而在我们仍然可以解决问题时获得反馈将真正帮助我们。告诉我们你忘记在文档中涵盖的内容、缺失的示例或迁移指南中的空白,我们也十分感谢。

我们知道,某些移植工作会因对 GTK 3 的间接依赖而被迫停止。例如,如果你正在使用 webkit webview 或 GtkSourceView 或 vte,你可能会发现很难尝试 GTK 4。

值得庆幸的是,一些库的移植工作已经顺利进行。其他库,例如 libgweather,将需要一些工作来将其核心功能与 GTK 3 依赖项分开。

我可以提供帮助吗?

如上一节所述,任何关于新 API、文档和移植指南的反馈都非常受欢迎和有帮助。

在许多其他领域,我们也可以使用帮助。如果你熟悉 OS X API,你可以在完成 macOS 后端方面做出真正的贡献。

我们也开始集成基于 ANGLE 的 GL 渲染器,但在我们利用它之前,我们的着色器需要 使其与 EGL 配合使用。在这方面的帮助将不胜感激。

下一步是什么?

我们致力于在年底前发布 GTK 4。从现在到那时,我们正在进行更多关于可访问性后端、改进 macOS 后端、编写文档和示例的工作。

关于 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>

这里有一些正在运行的 GtkDropDowns 示例

总结

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

GTK 4 中的可缩放列表

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

历史:树视图和列表框

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

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

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

在为所有渲染仍然使用小部件的同时,克服可扩展性限制一直是我们的长期路线图。

可扩展性限制
小部件 可扩展性
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 文档中的详细介绍

我们现在已将列表视图基础设施合并到 master 中。这并不意味着它已经完成。但是我们认为它已经准备好进行更广泛的应用,我们希望得到您的反馈,了解哪些方面运行良好,哪些方面运行不佳以及缺少哪些方面。

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

GTK 4 中的媒体

显示移动的图片越来越重要。GTK 4 将使 GTK 应用程序更容易显示动画;无论是程序化动画、webm 文件还是直播流。

一切皆可绘制

在查看动画之前,值得花一点时间了解 GTK 用于可以绘制的内容的底层抽象。在 GTK 2 和 3 中,这主要是 GdkPixbuf:您加载一个文件,然后得到一个像素数据块(或多或少采用一种格式)。如果您想对其进行动画处理,则可以使用 GdkPixbufAnimation,但公平地说,它不是一个非常成功的 API。

GTK 4 带来了一个名为 GdkPaintable 的新 API,它受到了 CSS Houdini 工作成果的启发。它非常灵活——任何可以合理绘制的东西都可以是 GdkPaintable。内容可以是可调整大小的(如 svg),也可以随着时间而变化(如 webm)。

通常显示图像内容的小部件(如 GtkImage 或 GtkPicture)知道如何使用可绘制对象。过去以某种形式生成像素数据的许多内容现在可以表示为可绘制对象:纹理图标,甚至是小部件

如果您有更专业的需求,可以使用 gtk_snapshot_to_paintable() 将任何可以在 GtkSnapshot 中捕获的内容转换为可绘制对象。如果您制作一个想要绘制可绘制对象的自定义小部件,则非常简单。只需调用 gdk_paintable_snapshot()

获得动画效果

正如我之前所说,可绘制对象可以随着时间推移而改变其内容。它们所需要做的就是发出 ::contents-changed 信号,GtkPicture 之类的小部件将执行正确的操作并更新其显示。

那么,我们从哪里获得一个改变内容的 GdkPaintable?我们可以使用 GTK 4 的内置 GtkMediaFile API 从文件中加载它。这是一个高级 API,类似于 GstPlayer:您将 uri 放入其中,然后得到一个具有 play() 函数和 pause() 函数的对象,并用作可绘制对象。

GTK 提供了 GtkMediaFile 的两种实现,一种使用 gstreamer,另一种使用 ffmpeg。由于我们不想让这两个成为 GTK 的硬性依赖,所以它们是可加载的模块。

您可以打开 GTK 检查器来找出正在使用的是哪一个。

保持控制

GtkMediaFile API 是 gtk4-widget-factory 在其首页演示动画 GTK 徽标所使用的 API。

正如您所看到的,它不仅仅是一个移动的画面,那里还有媒体控制——您可以通过使用 GtkVideo 小部件免费获得这些。

超越基础

从文件中加载动画可能不是那么令人兴奋,所以这里有另一个 示例,它更进一步。这是一个周末小项目,结合了 GtkVideo、libportalpipewire 来演示如何在 GTK 应用程序中显示视频流。

坏消息是,我们尚未为支持性的粘合代码(一个 GstSink、一个 GdkPaintable 和一个 GtkMediaStream)找到永久的归宿。它不适合放入 GTK,因为正如前面提到的,我们不想依赖 gstreamer,它也不适合放入 gstreamer,因为 GTK 4 尚未发布。我们肯定会在不久的将来解决这个问题,因为将 gstreamer 管道转换为几行代码的可绘制对象非常方便。

好消息是,代码的核心只有几行。

fd = xdp_portal_open_pipewire_remote_for_camera (portal);
stream = gtk_gst_media_stream_new_for_pipewire_fd (fd, NULL);
gtk_video_set_media_stream (video, stream);

 

GTK 4 中的自定义小部件 – 操作

(这是关于 GTK 4 中自定义小部件的系列文章的第五部分。第一部分第二部分第三部分第四部分)。

激活所有内容

GTK 中的许多内容都可以被激活:按钮、复选框、开关、菜单项等等。通常,可以用多种方式实现相同的任务,例如,将选择复制到剪贴板可以通过 Control-C 快捷键和上下文菜单中的项目来实现。

在 GTK 内部,有许多方式可以进行:可以发出信号(::activate,或 ::mnemonic-activate,或按键绑定信号)、可以调用回调,或者可以激活 GAction。这些在 GTK 4 中并非完全是新的,但我们正在朝着使用 GActions 作为连接操作的主要机制的方向发展。

操作

操作在 GTK 应用程序中可以以各种形式出现。

首先,有全局应用程序操作,添加到 GtkApplication 或 GtkApplicationWindow(这两个都实现了 GActionGroup 接口)。这是操作在 GTK 3 中首次出现的地方,主要目的是将其导出到会话总线上,以便与应用程序菜单一起使用。

我们还允许通过调用 gtk_widget_insert_action_group() 将操作与小部件关联。以这种方式添加的操作仅在激活源自层级结构中小部件下方时才会被考虑。

在 GTK 4 中创建操作的新方法是通过 gtk_widget_class_install_action() 在 class_init 函数中声明操作,类似于使用 g_object_class_install_property() 声明属性的方式。以这种方式创建的操作对于小部件的每个实例都可用。

这是来自 GtkColorSwatch 的示例。

gtk_widget_class_install_action (widget_class,
                                 "color.customize", "(dddd)",
                                 customize_color);

当 color.customize 操作被激活时,将调用 customize_color 函数。正如您所看到的,操作可以声明它们期望参数。这是使用 GVariant 语法;您需要提供四个双精度值。

一个方便的简写允许您创建一个有状态的操作来设置您的小部件类的属性。

gtk_widget_class_install_property_action (widget_class,
                                         "misc.toggle-visibility",
                                         "visibility");

这声明了一个名为 misc.toggle-visibility 的操作,该操作会切换布尔值 visibility 属性的值。

可操作对象和菜单

声明操作只是第一步,您还需要以某种形式将操作连接到 UI。对于实现 actionable 接口的按钮或开关等小部件,这就像设置 action-name 属性一样简单。

gtk_actionable_set_action_name (GTK_ACTIONABLE (button),
                                "misc.toggle-visibility");

当然,您也可以在 ui 文件中执行此操作。

如果您想从菜单中激活您的操作,您可能会使用从 XML 构建的菜单模型,例如这样:

<menu id="menu">
  <section>
    <item>
      <attribute name="label">Show text</attribute>
      <attribute name="action">misc.toggle-visibility</attribute>
    </item>
  </section>
</menu>

在 GTK 3 中,您将连接到 ::populate-popup 信号,以将项目添加到标签或条目的上下文菜单中。在 GTK 4 中,这是通过将菜单模型添加到小部件来完成的。

gtk_entry_set_extra_menu (entry, menu_model);

深入了解

要了解有关 GTK 4 中操作的更多信息,您可以阅读 GTK 文档中的 操作概述

GTK 4 中的自定义小部件 – 输入

(这是关于 GTK 4 中自定义小部件的系列文章的第四部分。第一部分第二部分第三部分)。

事件处理程序接管

在前面的部分中,我们已经看到了一些示例,其中处理 GtkWidget 信号被一些辅助对象取代。在输入区域,这种趋势更加明显,我们传统上有很多信号要处理:::button-press-event、::key-press-event、::touch-event 等等。所有这些信号在 GTK 4 中都消失了,取而代之的是您需要在您的小部件中添加事件控制器,并监听它们的信号。例如,有 GtkGestureClick、GtkEventControllerKey、GtkGestureLongPress 等等。

可以在 ui 文件中创建事件控制器,但在 init() 函数中创建更常见。

static void click_cb (GtkGestureClick *gesture,
                      int              n_press,
                      double           x,
                      double           y)
{
  GtkEventController *controller = GTK_EVENT_CONTROLLER (gesture);
  GtkWidget *widget = gtk_event_controller_get_widget (controller);

  if (x < gtk_widget_get_width (widget) / 2.0 &&
      y < gtk_widget_get_height (widget) / 2.0)
     g_print ("Red!\n");
}

...

  controller = gtk_gesture_click_new ();
  g_signal_handler_connect (controller, "pressed",
                            G_CALLBACK (click_cb), NULL);
  gtk_widget_add_controller (widget, controller);

gtk_widget_add_controller() 获取控制器的所有权,并且 GTK 在小部件最终确定时自动清理控制器,因此无需再做任何事情。

复杂的事件处理程序

前面章节中事件处理程序的示例很简单,一次只处理单个事件。手势有点复杂,因为它们处理相关的事件序列,并且通常会保留状态。

更复杂的事件处理程序的示例包括 DND 和键盘快捷键等。我们可能会在以后的文章中介绍其中的一些。

深入了解

所有不同事件处理程序背后的统一原则是,GTK 会将从窗口系统接收到的事件从窗口小部件树的根部传播到目标小部件,然后再返回,这种模式通常称为捕获-冒泡。

在键盘事件的情况下,目标小部件是当前焦点。对于指针事件,它是指针下的悬停小部件。

要阅读有关 GTK 中输入处理的更多信息,请访问 GTK 文档中的 输入处理概述。

展望

我们已经到了本系列准备材料的结尾。如果有人感兴趣,它可能会在将来的某个时候继续。可能的主题包括:快捷方式、操作和激活、拖放、焦点处理或可访问性。