跳转到内容

GTK+ 实例/树视图/树模型

来自 Wikibooks,开放世界中的开放书籍

GtkTreeModels 用于数据存储:GtkListStore 和 GtkTreeStore

[编辑 | 编辑源代码]

重要的是要了解 GtkTreeModel 是什么以及它不是什么。GtkTreeModel 本质上只是一个数据存储区的“接口”,这意味着它是一组标准化的函数,允许 GtkTreeView 小部件(以及应用程序程序员)查询数据存储区的某些特征,例如有多少行,哪些行有子节点,以及特定行有多少子节点。它还提供函数从数据存储区检索数据,并告诉树视图模型中存储了哪种类型的数据。每个数据存储区都必须实现 GtkTreeModel 接口并提供这些函数,您可以使用 GTK_TREE_MODEL(store) 将存储区转换为树模型来使用这些函数。GtkTreeModel 本身只提供一种方法来查询数据存储区的特征并检索现有数据,它不提供一种方法来删除或向存储区添加行或将数据放入存储区。这是使用特定存储区的函数来完成的。

Gtk+ 附带两个内置数据存储区(模型):GtkListStore 和 GtkTreeStore。顾名思义,GtkListStore 用于简单的数据项列表,这些项没有分层父子关系,而 GtkTreeStore 用于树状数据结构,其中项可以有父子关系。目录中的文件列表将是简单列表结构的示例,而目录树是树结构的示例。列表本质上只是一个没有子节点的树的特例,因此也可以使用树存储区来维护简单的数据项列表。GtkListStore 存在的唯一原因是提供更简单的接口,该接口不需要满足子父关系,并且因为简单列表模型可以针对没有子节点的特殊情况进行优化,这使得它更快、更高效。

GtkListStore 和 GtkTreeStore 应该满足应用程序开发人员可能想要在 GtkTreeView 中显示的大多数类型的数据。但是,需要注意的是,GtkListStore 和 GtkTreeStore 的设计考虑到了灵活性。如果您打算存储大量数据或有大量行,您应该考虑实现自己的自定义模型,以您自己的方式存储和操作数据,并实现 GtkTreeModel 接口。这不仅会更有效,而且从长远来看可能会产生更合理的代码,并让您对数据有更多控制。有关如何实现自定义模型的更多详细信息,请参见下文。

一旦您将 GtkTreeView 配置为显示您想要的内容,GtkListStore 和 GtkTreeStore 等树模型实现将为您处理视图方面。如果您更改存储区中的数据,模型将通知树视图,您的数据显示将更新。如果您添加或删除行,模型也会通知存储区,您的行也会出现在视图中或从视图中消失。3.1. 数据在存储区中的组织方式

模型(数据存储区)有模型列和行。虽然树视图会将模型中的每一行显示为视图中的一行,但模型的列不要与视图的列混淆。模型列表示具有固定数据类型的项的特定数据字段。您需要在创建列表存储区或树存储区时知道要存储哪种类型的数据,因为您以后不能添加新的字段。

例如,我们可能想要显示文件列表。我们将创建一个具有两个字段的列表存储区:一个存储文件名(即字符串)的字段,一个存储文件大小(即无符号整数)的字段。文件名将存储在模型的第 0 列中,文件大小将存储在模型的第 1 列中。对于每个文件,我们将向列表存储区添加一行,并将行的字段设置为文件名和文件大小。

GLib 类型系统 (GType) 用于指示模型列中存储了哪种类型的数据。以下是最常用的类型

  • G_TYPE_BOOLEAN
  • G_TYPE_INT、G_TYPE_UINT
  • G_TYPE_LONG、G_TYPE_ULONG、G_TYPE_INT64、G_TYPE_UINT64(这些在早期的 gtk+-2.0.x 版本中不受支持)
  • G_TYPE_FLOAT、G_TYPE_DOUBLE
  • G_TYPE_STRING - 在存储区中存储字符串(复制原始字符串)
  • G_TYPE_POINTER - 存储指针值(不将任何数据复制到存储区,只存储指针值!)
  • GDK_TYPE_PIXBUF - 在存储区中存储 GdkPixbuf(增加 pixbuf 的引用计数,见下文)

您不需要了解类型系统,知道上面的类型通常就足够了,这样您就可以告诉列表存储区或树存储区要存储哪种类型的数据。高级用户可以从基本的 GLib 类型派生自己的类型。对于简单的结构,您可以例如注册新的 boxed 类型,但这通常没有必要。G_TYPE_POINTER 通常也能做到,您只需要自己处理内存分配和释放。

存储 GObject 派生类型(大多数 GDK_TYPE_FOO 和 GTK_TYPE_FOO)是一种特殊情况,将在下面进一步处理。

以下是如何创建列表存储区的示例

  GtkListStore *list_store;

  list_store = gtk_list_store_new (2, G_TYPE_STRING, G_TYPE_UINT);

这将创建一个新的列表存储区,其中包含两列。第 0 列存储字符串,第 1 列存储每行的无符号整数。当然,此时模型还没有任何行。在我们开始添加行之前,让我们看看引用特定行的不同方法。

引用行:GtkTreeIter、GtkTreePath、GtkTreeRowReference

[编辑 | 编辑源代码]

引用特定行有不同的方法。您必须处理的两个方法是 GtkTreeIter 和 GtkTreePath。

GtkTreePath

[编辑 | 编辑源代码]

以“地理”方式描述行

GtkTreePath 是一种比较直接的方法来描述模型中行的逻辑位置。由于 GtkTreeView 总是显示模型中的所有行,因此树路径在模型和视图中始终描述相同的行。

Figure 3-1.

图片显示了树路径的字符串形式,旁边是标签。基本上,它只是从树视图的假想根节点开始计算子节点。空的树路径字符串将指定该假想的不可见根节点。现在“歌曲”是第一个子节点(从根节点开始),因此它的树路径只是“0”。“视频”是根节点的第二个子节点,它的树路径是“1”。“oggs”是根节点第一个项目(“歌曲”)的第二个子节点,因此它的树路径是“0:1”。因此,您只需要从根节点向下数到要查找的行,您就会得到您的树路径。

为了说明这一点,树路径“3:9:4:1”在人类语言中基本意味着(注意 - 这不是它的真正含义!)类似于:转到第 3 行顶级行。现在转到该行的第 9 个子节点。继续到先前行的第 4 个子节点。然后继续到该行的第 1 个子节点。现在您就位于此树路径所描述的行。但这并不是它对 Gtk+ 的意义。虽然人类从 1 开始计数,但计算机通常从 0 开始计数。因此,树路径“3:9:4:1”的真正含义是:转到第 4 行顶级行。然后转到该行的第 10 个子节点。选择该行的第 5 个子节点。然后继续到先前行的第 2 个子节点。现在您就位于此树路径所描述的行。:)

这种引用行的含义如下:如果您在中间插入或删除行,或者对行进行排序,则树路径可能会突然引用与插入/删除/排序之前完全不同的行。这一点非常重要。(请参阅下面有关 GtkTreeRowReferences 的部分,了解一种树路径,该路径会不断更新自身以确保它在模型发生更改时始终引用相同的行)。

如果您想象如果我们从上面的图片中的树中删除名为“有趣片段”的行会发生什么,就会显现出这种影响。“电影预告片”行将突然成为“片段”的第一个也是唯一的子节点,并由以前属于“有趣片段”的树路径描述,即“1:0:0”。

您可以使用 gtk_tree_path_new_from_string 从字符串形式的路径获取新的 GtkTreePath,还可以使用 gtk_tree_path_to_string 将给定的 GtkTreePath 转换为其字符串表示形式。通常您很少需要处理字符串表示形式,它在这里只是为了演示树路径的概念。

GtkTreePath 在内部使用整数数组而不是字符串表示形式。您可以使用 gtk_tree_path_get_depth 获取树路径的深度(即嵌套级别)。深度为 0 是树视图和模型的假想的不可见根节点。深度为 1 表示树路径描述了顶级行。由于列表只是没有子节点的树,因此列表中的所有行始终具有深度为 1 的树路径。gtk_tree_path_get_indices 返回树路径的内部整数数组。您也几乎不需要操作它们。

如果您使用树路径操作,您最有可能使用给定的树路径,并使用诸如 gtk_tree_path_up、gtk_tree_path_down、gtk_tree_path_next、gtk_tree_path_prev、gtk_tree_path_is_ancestor 或 gtk_tree_path_is_descendant 之类的函数。请注意,通过这种方式,您可以构建和操作引用模型或视图中不存在的行的树路径!检查路径是否对特定模型有效(即路径描述的行存在)的唯一方法是使用 gtk_tree_model_get_iter 将路径转换为迭代器。

GtkTreePath 是一个不透明的结构,其细节对编译器隐藏。如果您需要复制树路径,请使用 gtk_tree_path_copy。

GtkTreeIter

[编辑 | 编辑源代码]

用模型语言引用行

引用列表或树中行的另一种方法是 GtkTreeIter。树迭代器只是一个结构,它包含几个指针,这些指针对您使用的模型有意义。树迭代器在模型内部使用,并且它们通常包含指向所讨论行的内部数据的直接指针。您永远不应该查看树迭代器的内容,也不应该直接修改它。

所有树模型(因此也包括 GtkListStore 和 GtkTreeStore)都必须支持对树迭代器进行操作的 GtkTreeModel 函数(例如,获取由给定树迭代器指定的行的第一个子级的树迭代器,获取列表/树中的第一行,获取给定迭代器的第 n 个子级等等)。其中一些函数是

  • gtk_tree_model_get_iter_first - 将给定的迭代器设置为列表或树中的第一个顶层项目
  • gtk_tree_model_iter_next - 将给定的迭代器设置为列表或树中当前级别的下一个项目。
  • gtk_tree_model_iter_children - 将第一个给定的迭代器设置为第二个迭代器引用的行的第一个子级(对列表不太有用,主要对树有用)。
  • gtk_tree_model_iter_n_children - 返回由提供的迭代器引用的行具有的子级数量。如果您传递 NULL 而不是指向迭代器结构的指针,则此函数将返回顶层行的数量。您也可以使用此函数来计算列表存储中的项目数量。
  • gtk_tree_model_iter_nth_child - 将第一个迭代器设置为第二个迭代器引用的行的第 n 个子级。如果您传递 NULL 而不是指向迭代器结构的指针作为第二个迭代器,则可以将第一个迭代器设置为列表的第 n 行。
  • gtk_tree_model_iter_parent - 将第一个迭代器设置为第二个迭代器引用的行的父级(对列表不做任何操作,仅对树有用)。

几乎所有这些函数在请求的操作成功时返回 TRUE,否则返回 FALSE。还有更多对迭代器进行操作的函数。查看 GtkTreeModel API 参考以了解更多详细信息。

您可能会注意到没有 gtk_tree_model_iter_prev。由于各种原因,这不太可能实现。但是,一旦您阅读了本节,编写提供此功能的辅助函数应该相当简单。

树迭代器用于从存储中检索数据,以及将数据放入存储中。如果您使用 gtk_list_store_append 或 gtk_tree_store_append 将新行添加到存储中,您也会得到一个树迭代器作为结果。

树迭代器通常只在很短的时间内有效,如果存储发生变化,可能会变得无效。因此,通常存储树迭代器是一个不好的主意,除非您确实知道自己在做什么。您可以使用 gtk_tree_model_get_flags 获取模型的标志,并检查是否设置了 GTK_TREE_MODEL_ITERS_PERSIST 标志(在这种情况下,只要行存在,树迭代器就会有效),但仍然不建议存储迭代器结构,除非您真的打算这样做。有一种更好的方法来跟踪一段时间内的行:GtkTreeRowReference

GtkTreeRowReference

[编辑 | 编辑源代码]

即使模型发生变化也要跟踪行

GtkTreeRowReference 本质上是一个对象,它获取一个树路径,并监视模型的变化。如果任何内容发生变化,例如行被插入或删除,或者行被重新排序,树行引用对象将使给定的树路径保持最新,以便它始终指向与以前相同的行。如果给定的行被删除,则树行引用将变得无效。

可以使用 gtk_tree_row_reference_new 创建一个新的树行引用,给定一个模型和一个树路径。之后,树行引用将在模型发生变化时不断更新路径。可以使用 gtk_tree_row_reference_get_path 检索最初创建树行引用时引用的行的当前树路径。如果行已被删除,则将返回 NULL 而不是树路径。返回的树路径是副本,因此在不再需要时需要使用 gtk_tree_path_free 释放它。

您可以使用 gtk_tree_row_reference_valid 检查引用的行是否仍然存在,并在不再需要时使用它释放它。

对于好奇的人:在内部,树行引用连接到树模型的“row-inserted”、“row-deleted”和“rows-reordered”信号,并在模型发生影响引用的行位置的更改时更新其内部树路径。

请注意,使用树行引用会带来少量开销。这对于 99.9% 的应用程序来说几乎可以忽略不计,但是当您有数千行和/或行引用时,这可能是需要牢记的事情(因为每当行被插入、删除或重新排序时,都会为每个行引用发送和处理一个信号)。

如果您到目前为止只阅读了本教程,那么很难真正解释树行引用有什么用。在下面关于一次删除多行的部分中可以找到树行引用派上用场的示例。

在实践中,程序员可以使用树行引用来跟踪一段时间内的行,或者直接存储树迭代器(如果,且仅当模型具有持久迭代器时)。GtkListStore 和 GtkTreeStore 都具有持久迭代器,因此存储迭代器是可能的。但是,使用树行引用绝对是正确的方式(tm)来做事,即使它会带来一些开销,这些开销可能会影响具有大量行的树的性能(在这种情况下,可能更适合编写自定义模型)。特别是初学者可能会发现处理和存储树行引用比迭代器更容易,因为树行引用由指针值处理,您可以轻松地将其添加到 GList 或指针数组中,而存储树迭代器则很容易以错误的方式存储。

树迭代器可以使用 gtk_tree_model_get_path 轻松转换为树路径,而树路径可以使用 gtk_tree_model_get_iter 轻松转换为树迭代器。以下是一个示例,展示了如何在“row-activated”信号回调中从树视图传递给我们的树路径中获取迭代器。我们需要迭代器才能从存储中检索数据

/************************************************************
 *                                                          *
 * Converting a GtkTreePath into a GtkTreeIter              *
 *                                                          *
 ************************************************************/

/************************************************************
 *
 * onTreeViewRowActivated: a row has been double-clicked
 *
 ************************************************************/

void
onTreeViewRowActivated (GtkTreeView *view, GtkTreePath *path,
                        GtkTreeViewColumn *col, gpointer userdata)
{
	GtkTreeIter   iter;
  GtkTreeModel *model;

  model = gtk_tree_view_get_model(view);

  if (gtk_tree_model_get_iter(model, &iter, path))
  {
    gchar *name;

    gtk_tree_model_get(model, &iter, COL_NAME, &name, -1);

    g_print ("The row containing the name '%s' has been double-clicked.\n", name);

    g_free(name);
  }
}

树行引用使用 gtk_tree_row_reference_get_path 揭示行的当前路径。没有从树行引用直接获取树迭代器的方法,您必须先检索树行引用的路径,然后将其转换为树迭代器。

由于树迭代器仅在很短的时间内有效,因此它们通常在堆栈上分配,如下面的示例所示(请记住 GtkTreeIter 只是一个结构,它包含您不需要了解的任何数据字段)

 /************************************************************
  *                                                          *
  *  Going through every row in a list store                 *
  *                                                          *
  ************************************************************/

  void
  traverse_list_store (GtkListStore *liststore)
  {
    GtkTreeIter  iter;
    gboolean     valid;

    g_return_if_fail ( liststore != NULL );

    /* Get first row in list store */
    valid = gtk_tree_model_get_iter_first(GTK_TREE_MODEL(liststore), &iter);

    while (valid)
    {
       /* ... do something with that row using the iter ...          */
       /* (Here column 0 of the list store is of type G_TYPE_STRING) */
       gtk_list_store_set(liststore, &iter, 0, "Joe", -1);

       /* Make iter point to the next row in the list store */
       valid = gtk_tree_model_iter_next(GTK_TREE_MODEL(liststore), &iter);
    }
  }

上面的代码要求模型填充迭代器结构以使其指向列表存储中的第一行。如果存在第一行且列表存储不为空,则将设置迭代器,并且 gtk_tree_model_get_iter_first 将返回 TRUE。如果没有第一行,它将只返回 FALSE。如果存在第一行,则将进入 while 循环,我们更改第一行的一些数据。然后我们要求模型使给定的迭代器指向下一行,直到没有更多行,此时 gtk_tree_model_iter_next 返回 FALSE。我们也可以使用 gtk_tree_model_foreach 来遍历列表存储,而不是遍历列表存储

向存储中添加行

[编辑 | 编辑源代码]

向列表存储中添加行

[编辑 | 编辑源代码]

行使用 gtk_list_store_append 添加到列表存储中。这将在列表的末尾插入一个新的空行。还有其他函数在 GtkListStore API 参考中记录,这些函数可以让您更精确地控制新行插入的位置,但由于它们的工作原理与 gtk_list_store_append 非常相似,并且使用起来相当简单,因此我们在这里不再讨论它们。

以下是如何创建一个列表存储并向其中添加一些(空)行的简单示例

  GtkListStore *liststore;
  GtkTreeIter   iter;

  liststore = gtk_list_store_new(1, G_TYPE_STRING);

  /* Append an empty row to the list store. Iter will point to the new row */
  gtk_list_store_append(liststore, &iter);

  /* Append an empty row to the list store. Iter will point to the new row */
  gtk_list_store_append(liststore, &iter);

  /* Append an empty row to the list store. Iter will point to the new row */
  gtk_list_store_append(liststore, &iter);

当然,这本身还不太有用。在下一节中,我们将向行添加数据。

向树存储器添加行

[edit | edit source]

向树存储器添加行与向列表存储器添加行类似,只是使用 `gtk_tree_store_append` 函数,并且需要一个额外的参数,即要插入行的父级的树迭代器。如果提供 `NULL` 而不是提供另一个行的树迭代器,则将插入一个新的顶级行。如果提供了父级树迭代器,则新空行将插入在父级任何已存在的子级之后。同样,还有其他方法可以将行插入树存储器,它们在 `GtkTreeStore` API 参考手册中有所说明。另一个简短的示例

  GtkListStore *treestore;
  GtkTreeIter   iter, child;

  treestore = gtk_tree_store_new(1, G_TYPE_STRING);

  /* Append an empty top-level row to the tree store.
   *  Iter will point to the new row */
  gtk_tree_store_append(treestore, &iter, NULL);

  /* Append another empty top-level row to the tree store.
   *  Iter will point to the new row */
  gtk_tree_store_append(treestore, &iter, NULL);

  /* Append a child to the row we just added.
   *  Child will point to the new row */
  gtk_tree_store_append(treestore, &child, &iter);

  /* Get the first row, and add a child to it as well (could have been done
   *  right away earlier of course, this is just for demonstration purposes) */
  if (gtk_tree_model_get_iter_first(GTK_TREE_MODEL(treestore), &iter))
  {
    /* Child will point to new row */
    gtk_tree_store_append(treestore, &child, &iter);
  }
  else
  {
    g_error("Oops, we should have a first row in the tree store!\n");
  }

添加大量行时的速度问题

[edit | edit source]

一种常见的场景是,模型需要在某个时刻用大量行填充,无论是启动时,还是打开某个文件时。同样常见的场景是,即使在强大的机器上,一旦模型包含超过几千行,这也会花费相当长的时间,并且插入速度呈指数下降。正如上面已经指出的,在这种情况下,编写自定义模型可能是最好的方法。然而,有一些方法可以解决这个问题,即使使用标准的 `Gtk+` 模型,也可以加快速度。

首先,在进行大量插入之前,应该将列表存储器或树存储器从树视图中分离,然后进行插入,只有在完成插入后,才将存储器重新连接到树视图。像这样

  ...

  model = gtk_tree_view_get_model(GTK_TREE_VIEW(view));

  g_object_ref(model); /* Make sure the model stays with us after the tree view unrefs it */

  gtk_tree_view_set_model(GTK_TREE_VIEW(view), NULL); /* Detach model from view */

  ... insert a couple of thousand rows ...

  gtk_tree_view_set_model(GTK_TREE_VIEW(view), model); /* Re-attach model to view */

  g_object_unref(model);

  ...

其次,在进行大量插入时,应该确保排序已禁用,否则存储器可能会在每次插入行后重新排序,这将非常慢。

第三,如果有很多行,不应该保留大量树行引用,因为每次插入(或删除)时,每个树行引用都会检查其路径是否需要更新。

操作行数据

[edit | edit source]

向数据存储器添加空行并不是很令人兴奋,所以让我们看看如何添加或更改存储器中的数据。

`gtk_list_store_set` 和 `gtk_tree_store_set` 用于操作给定行的数据。还有 `gtk_list_store_set_value` 和 `gtk_tree_store_set_value`,但这些函数应该只供熟悉 GLib 的 `GValue` 系统的人使用。

`gtk_list_store_set` 和 `gtk_tree_store_set` 都接受可变数量的参数,并且必须以 `-1` 参数结尾。前两个参数是指向模型的指针,以及指向我们要更改其数据的行的迭代器。它们之后是可变数量的 (列,数据) 参数对,以 `-1` 结尾。列指的是模型列号,通常是枚举值(为了使代码更易读,并使更改更容易)。数据应该与模型列的数据类型相同。

这是一个示例,我们创建一个存储器,它为每一行存储两个字符串和一个整数

  enum
  {
    COL_FIRST_NAME = 0,
    COL_LAST_NAME,
    COL_YEAR_BORN,
    NUM_COLS
  };

  GtkListStore *liststore;
  GtkTreeIter   iter;

  liststore = gtk_list_store_new(NUM_COLS, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_UINT);

  /* Append an empty row to the list store. Iter will point to the new row */
  gtk_list_store_append(liststore, &iter);

  /* Fill fields with some data */
  gtk_list_store_set (liststore, &iter,
                      COL_FIRST_NAME, "Joe",
                      COL_LAST_NAME, "Average",
                      COL_YEAR_BORN, (guint) 1970,
                      -1);

你不必担心为要存储的数据分配和释放内存。模型(更准确地说是 GLib/GObject `GType` 和 `GValue` 系统)将为你处理这个问题。例如,如果你存储一个字符串,模型将复制该字符串并存储它。如果你稍后将该字段设置为新的字符串,模型将自动释放旧字符串,并再次复制新字符串并存储该副本。这适用于几乎所有类型,无论是 `G_TYPE_STRING` 还是 `GDK_TYPE_PIXBUF`。

需要注意的例外是 `G_TYPE_POINTER`。如果你分配一块数据或一个复杂结构,并将其存储在 `G_TYPE_POINTER` 字段中,则只存储指针值。模型不知道你的指针引用的数据的尺寸或内容,因此即使它想复制,它也不能复制,所以你必须自己分配和释放内存。然而,如果你不想自己这样做,并希望模型为你处理自定义数据,那么你需要注册自己的类型,并将其从 GLib 基本类型之一(通常是 `G_TYPE_BOXED`)派生。有关详细信息,请参阅 `GObject GType` 参考手册。复制数据当然涉及内存分配和其他开销,因此在采取这种方法之前,应该仔细考虑使用自定义 GLib 类型而不是 `G_TYPE_POINTER` 的性能影响。同样,自定义模型可能是更好的选择,具体取决于要存储(和检索)的总数据量。

检索行数据

[edit | edit source]

如果不能再次检索数据,那么存储数据就没有什么用处。这可以使用 `gtk_tree_model_get` 完成,它接受与 `gtk_list_store_set` 或 `gtk_tree_store_set` 类似的参数,只是它接受 (列,指针) 参数。指针必须指向与存储在该特定模型列中的数据类型相同的变量。

以下是之前的示例,扩展到遍历列表存储器并打印出存储的数据。作为额外内容,我们使用 `gtk_tree_model_foreach` 遍历存储器,并从 `foreach` 回调函数中传递给我们的 `GtkTreePath` 中检索行号

  #include <gtk/gtk.h>

  enum
  {
    COL_FIRST_NAME = 0,
    COL_LAST_NAME,
    COL_YEAR_BORN,
    NUM_COLS
  };

  gboolean
  foreach_func (GtkTreeModel *model,
                GtkTreePath  *path,
                GtkTreeIter  *iter,
                gpointer      user_data)
  {
    gchar *first_name, *last_name, *tree_path_str;
    guint  year_of_birth;

    /* Note: here we use 'iter' and not '&iter', because we did not allocate
     *  the iter on the stack and are already getting the pointer to a tree iter */

    gtk_tree_model_get (model, iter,
                        COL_FIRST_NAME, &first_name,
                        COL_LAST_NAME, &last_name,
                        COL_YEAR_BORN, &year_of_birth,
                        -1);

    tree_path_str = gtk_tree_path_to_string(path);

    g_print ("Row %s: %s %s, born %u\n", tree_path_str,
             first_name, last_name, year_of_birth);

    g_free(tree_path_str);

    g_free(first_name); /* gtk_tree_model_get made copies of       */
    g_free(last_name);  /* the strings for us when retrieving them */

    return FALSE; /* do not stop walking the store, call us with next row */
  }

  void
  create_and_fill_and_dump_store (void)
  {
    GtkListStore *liststore;
    GtkTreeIter   iter;

    liststore = gtk_list_store_new(NUM_COLS, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_UINT);

    /* Append an empty row to the list store. Iter will point to the new row */
    gtk_list_store_append(liststore, &iter);

    /* Fill fields with some data */
    gtk_list_store_set (liststore, &iter,
                        COL_FIRST_NAME, "Joe",
                        COL_LAST_NAME, "Average",
                        COL_YEAR_BORN, (guint) 1970,
                        -1);

    /* Append another row, and fill in some data */
    gtk_list_store_append(liststore, &iter);

    gtk_list_store_set (liststore, &iter,
                        COL_FIRST_NAME, "Jane",
                        COL_LAST_NAME, "Common",
                        COL_YEAR_BORN, (guint) 1967,
                        -1);

    /* Append yet another row, and fill it */
    gtk_list_store_append(liststore, &iter);

    gtk_list_store_set (liststore, &iter,
                        COL_FIRST_NAME, "Yo",
                        COL_LAST_NAME, "Da",
                        COL_YEAR_BORN, (guint) 1873,
                        -1);

    /* Now traverse the list */

    gtk_tree_model_foreach(GTK_TREE_MODEL(liststore), foreach_func, NULL);
  }

  int
  main (int argc, char **argv)
  {
    gtk_init(&argc, &argv);

    create_and_fill_and_dump_store();

    return 0;
  }

请注意,当创建新行时,行的所有字段都设置为适合数据类型的默认 `NIL` 值。类型为 `G_TYPE_INT` 的字段将自动包含值 `0`,直到它被设置为不同的值,并且字符串和所有类型的指针类型将为 `NULL`,直到被设置为其他内容。这些是模型的有效内容,如果你不确定行内容是否已设置为某项内容,则需要准备好处理 `NULL` 指针等情况。

使用一个额外的空行运行上面的程序,并查看输出,以了解其效果。

释放检索到的行数据

[edit | edit source]

除非你正在处理类型为 `G_TYPE_POINTER` 的模型列,否则 `gtk_tree_model_get` 将始终复制检索到的数据。

对于字符串,这意味着当你不再需要它时,你需要使用 `g_free` 释放返回的字符串,如上面的示例所示。

如果你从存储器中检索到 `GdkPixbuf` 等 `GObject`,`gtk_tree_model_get` 将自动为其添加引用,因此你需要在完成对它的操作后调用 `g_object_unref`。

  ...

  GdkPixbuf *pixbuf;

  gtk_tree_model_get (model, &iter, 
                      COL_PICTURE, &pixbuf, 
                      NULL);

  if (pixbuf != NULL)
  {
    do_something_with_pixbuf (pixbuf);
    g_object_unref (pixbuf);
  }

  ...

类似地,从模型中检索的 `GBoxed` 派生类型需要在完成对它们的操作后使用 `g_boxed_free` 释放(如果你以前从未听说过 `GBoxed`,不要担心)。

如果模型列的类型是 `G_TYPE_POINTER`,`gtk_tree_model_get` 将只复制指针值,而不是数据(即使它想复制,它也不能复制数据,因为它不知道如何复制或确切地复制什么)。如果你在指针列中存储指向对象或字符串的指针(除非你真正知道自己在做什么以及为什么这样做,否则你应该不要这样做),你不需要像上面描述的那样取消引用或释放返回的值,因为 `gtk_tree_model_get` 将不知道它们是什么类型的数据,因此不会在检索时取消引用或复制它们。

删除行

[edit | edit source]

可以使用 `gtk_list_store_remove` 和 `gtk_tree_store_remove` 很容易地删除行。删除的行也会自动从树视图中删除,所有存储的数据也会自动释放,除了 `G_TYPE_POINTER` 列(参见上文)。

删除单行非常简单:你需要获取标识要删除行的迭代器,然后使用上述函数之一。这是一个简单的示例,当双击行时,它会删除该行(从用户界面角度来看很糟糕,但它只是一个示例)

  static void
  onRowActivated (GtkTreeView        *view,
                  GtkTreePath        *path,
                  GtkTreeViewColumn  *col,
                  gpointer            user_data)
  {
    GtkTreeModel *model;
    GtkTreeIter   iter;

    g_print ("Row has been double-clicked. Removing row.\n");

    model = gtk_tree_view_get_model(view);

    if (!gtk_tree_model_get_iter(model, &iter, path))
      return; /* path describes a non-existing row - should not happen */

    gtk_list_store_remove(GTK_LIST_STORE(model), &iter);
  }


  void
  create_treeview (void)
  {
    ...
    g_signal_connect(treeview, "row-activated", G_CALLBACK(onRowActivated), NULL);
    ...
  }

注意:`gtk_list_store_remove` 和 `gtk_tree_store_remove` 在 `Gtk+-2.0` 和 `Gtk+-2.2` 及更高版本中具有略微不同的语义。在 `Gtk+-2.0` 中,这两个函数都不返回值,而在更高版本的 `Gtk+` 中,这些函数返回 `TRUE` 或 `FALSE`,以指示给定的迭代器是否已设置为下一个有效行(或在没有下一个行时被无效化)。在编写针对所有 `Gtk+-2.x` 版本的代码时,这一点很重要。在这种情况下,你应该忽略返回的值(如上面的调用中),并在需要时使用 `gtk_list_store_iter_is_valid` 检查迭代器。

如果你想从列表中删除第 n 行(或树节点的第 n 个子节点),你有两种方法:要么首先创建一个描述该行的 `GtkTreePath`,然后将其转换为迭代器并删除它;要么获取父节点的迭代器,并使用 `gtk_tree_model_iter_nth_child`(如果使用 `NULL` 作为父节点迭代器,它也适用于列表存储器。当然,你也可以从第一个顶级行的迭代器开始,然后一步一步地将其移动到要删除的行,尽管这似乎是一种相当笨拙的方法。

以下代码片段将删除列表中的第 n 行(如果存在)

   /******************************************************************
    *
    *  list_store_remove_nth_row
    *
    *  Removes the nth row of a list store if it exists.
    *
    *  Returns TRUE on success or FALSE if the row does not exist.
    *
    ******************************************************************/

   gboolean
   list_store_remove_nth_row (GtkListStore *store, gint n)
   {
     GtkTreeIter  iter;

     g_return_val_if_fail (GTK_IS_LIST_STORE(store), FALSE);

     /* NULL means the parent is the virtual root node, so the
      *  n-th top-level element is returned in iter, which is
      *  the n-th row in a list store (as a list store only has
      *  top-level elements, and no children) */
     if (gtk_tree_model_iter_nth_child(GTK_TREE_MODEL(store), &iter, NULL, n))
     {
       gtk_list_store_remove(store, &iter);
       return TRUE;
     }

     return FALSE;
   }

删除多行

[edit | edit source]

一次删除多行有时可能有点棘手,需要考虑如何最好地完成此操作。例如,无法使用 `gtk_tree_model_foreach` 遍历存储器,在回调函数中检查给定的行是否应该删除,然后只通过调用存储器中的一个删除函数来删除它。这将不起作用,因为模型是从 `foreach` 循环内部更改的,这可能会突然使 `foreach` 函数中以前有效的树迭代器失效,从而导致不可预测的结果。

当然,您可以在循环中遍历存储,并在需要时调用 gtk_list_store_remove 或 gtk_tree_store_remove 来删除行,然后如果 remove 函数返回 TRUE(表示迭代器仍然有效,现在指向已删除行的下一行),则只需继续。但是,这种方法仅适用于 Gtk+-2.2 或更高版本,如果您希望您的程序与 Gtk+-2.0 也能编译和运行,则它将不起作用,因为上面概述的原因(在 Gtk+-2.0 中,remove 函数不会将传递的迭代器设置为下一个有效行)。此外,虽然这种方法对于列表存储可能是可行的,但对于树存储来说会有点尴尬。

以下是一个用于一次删除多行的替代方法的示例(这里我们想要从存储中删除所有包含出生日期在 1980 年之后的个人的行,但它也可以是所有选定的行或其他标准)

 /******************************************************************
  *
  *  Removing multiple rows in one go
  *
  ******************************************************************/

  ...

  gboolean
  foreach_func (GtkTreeModel *model,
                GtkTreePath  *path,
                GtkTreeIter  *iter,
                GList       **rowref_list)
  {
    guint  year_of_birth;

    g_assert ( rowref_list != NULL );

    gtk_tree_model_get (model, iter, COL_YEAR_BORN, &year_of_birth, -1);

    if ( year_of_birth > 1980 )
    {
      GtkTreeRowReference  *rowref;

      rowref = gtk_tree_row_reference_new(model, path);

      *rowref_list = g_list_append(*rowref_list, rowref);
    }

    return FALSE; /* do not stop walking the store, call us with next row */
  }

  void
  remove_people_born_after_1980 (void)
  {
     GList *rr_list = NULL;    /* list of GtkTreeRowReferences to remove */
     GList *node;

     gtk_tree_model_foreach(GTK_TREE_MODEL(store),
                            (GtkTreeModelForeachFunc) foreach_func,
                            &rr_list);

     for ( node = rr_list;  node != NULL;  node = node->next )
     {
        GtkTreePath *path;

        path = gtk_tree_row_reference_get_path((GtkTreeRowReference*)node->data);

        if (path)
        {
           GtkTreeIter  iter;

           if (gtk_tree_model_get_iter(GTK_TREE_MODEL(store), &iter, path))
           {
             gtk_list_store_remove(store, &iter);
           }

           /* FIXME/CHECK: Do we need to free the path here? */
        }
     }

     g_list_foreach(rr_list, (GFunc) gtk_tree_row_reference_free, NULL);
     g_list_free(rr_list);
  }

  ...


如果您想删除所有行,gtk_list_store_clear 和 gtk_tree_store_clear 会派上用场。

存储 GObjects(Pixbufs 等)

[编辑 | 编辑源代码]

一种特殊情况是 GObject 类型,例如 GDK_TYPE_PIXBUF,它们存储在列表或树存储中。存储不会复制对象,而是会增加对象的引用计数。如果不再需要对象(例如,在旧对象的位置存储了新对象,当前值被 NULL 替换,行被删除,或者存储被销毁),则存储会再次取消引用对象。

从开发人员的角度来看,这意味着如果您希望存储在不再需要时自动处理对象,则需要对刚刚添加到存储的对象进行 g_object_unref 操作。这是因为在对象创建时,对象有一个初始引用计数为 1,它是“您的”引用计数,并且只有当引用计数达到 0 时,对象才会被销毁。以下是 pixbuf 的生命周期

  GtkListStore *list_store;
  GtkTreeIter   iter;
  GdkPixbuf    *pixbuf;
  GError       *error = NULL;

  list_store = gtk_list_store_new (2, GDK_TYPE_PIXBUF, G_TYPE_STRING);

  pixbuf = gdk_pixbuf_new_from_file("icon.png", &error);

  /* pixbuf has a refcount of 1 after creation */

  if (error)
  {
    g_critical ("Could not load pixbuf: %s\n", error->message);
    g_error_free(error);
    return;
  }

  gtk_list_store_append(list_store, &iter);

  gtk_list_store_set(list_store, &iter, 0, pixbuf, 1, "foo", -1);

  /* pixbuf has a refcount of 2 now, as the list store has added its own reference */

  g_object_unref(pixbuf);

  /* pixbuf has a refcount of 1 now that we have released our initial reference */

  /* we don't want an icon in that row any longer */
  gtk_list_store_set(list_store, &iter, 0, NULL, -1);

  /* pixbuf has automatically been destroyed after its refcount has reached 0.
   *  The list store called g_object_unref() on the pixbuf when it replaced
   *  the object in the store with a new value (NULL). */

了解了如何向存储添加、操作和检索数据之后,下一步是将这些数据显示在 GtkTreeView 小部件中。

存储数据结构:指针、GBoxed 类型和 GObject(待办事项)

[编辑 | 编辑源代码]

未完成的章节。

华夏公益教科书