跳转到内容

GTK+ 示例/树视图/排序

来自维基教科书,开放的书籍,开放的世界

列表和树旨在进行排序。这使用 GtkTreeSortable 接口完成,该接口可以由树模型实现。'接口' 意味着你可以直接将 GtkTreeModel 转换为 GtkTreeSortable,使用 GTK_TREE_SORTABLE(model),并在其上使用已记录的树可排序函数,就像我们之前将列表存储转换为树模型并使用 gtk_tree_model_foo 函数族一样。GtkListStore 和 GtkTreeStore 都实现了树可排序接口。

对列表存储或树存储进行排序最直接的方法是直接在其上使用树可排序接口。这将在原位对存储进行排序,这意味着如果需要,行将在存储中重新排序。这具有树视图中行的位置将始终与模型中行的位置相同的优点,换句话说:引用视图中行的树路径将始终引用模型中的同一行,因此你可以使用 gtk_tree_model_get_iter 通过树视图提供的树路径轻松地获取行的迭代器。这不仅方便,而且足以满足大多数场景。

但是,在某些情况下,在原位对模型进行排序是不希望的,例如,当多个树视图以不同的排序显示相同的模型时,或者当模型的未排序状态具有特殊含义并且需要在某个时刻恢复时。这就是 GtkTreeModelSort 的作用,它是一个特殊的模型,它将子模型(例如列表存储或树存储)的未排序行映射到排序状态,而不会更改子模型。

GtkTreeSortable

[编辑 | 编辑源代码]

树可排序接口非常简单,应该易于使用。基本上,你为每个你可能希望按其排序的标准定义一个 '排序列 ID' 整数,并使用 gtk_tree_sortable_set_sort_func 告诉树可排序函数应该调用哪个函数来比较两个行(用两个树迭代器表示)的每个排序 ID。然后,你通过使用 gtk_tree_sortable_set_sort_column_id 设置排序列 ID 和排序顺序来对模型进行排序,模型将使用你设置的比较函数重新排序。你的排序列 ID 可以对应于你的模型列,但它们不必(你可能希望根据未直接由单个模型列中的数据表示的标准进行排序,例如)。一些代码来说明这一点

  enum
  {
    COL_NAME = 0,
    COL_YEAR_BORN
  };


  enum
  {
    SORTID_NAME = 0,
    SORTID_YEAR
  };


  GtkTreeModel  *liststore = NULL;


  void
  toolbar_onSortByYear (void)
  {
    GtkTreeSortable *sortable;
    GtkSortType      order;
    gint             sortid;

    sortable = GTK_TREE_SORTABLE(liststore);

    /* If we are already sorting by year, reverse sort order,
     *  otherwise set it to year in ascending order */

    if (gtk_tree_sortable_get_sort_column_id(sortable, &sortid, &order) == TRUE
          &&  sortid == SORTID_YEAR)
    {
      GtkSortType neworder;

      neworder = (order == GTK_SORT_ASCENDING) ? GTK_SORT_DESCENDING : GTK_SORT_ASCENDING;

      gtk_tree_sortable_set_sort_column_id(sortable, SORTID_YEAR, neworder);
    }
    else
    {
      gtk_tree_sortable_set_sort_column_id(sortable, SORTID_YEAR, GTK_SORT_ASCENDING);
    }
  }


  /* This is not pretty. Of course you can also use a
   *  separate compare function for each sort ID value */

  gint
  sort_iter_compare_func (GtkTreeModel *model,
                          GtkTreeIter  *a,
                          GtkTreeIter  *b,
                          gpointer      userdata)
  {
    gint sortcol = GPOINTER_TO_INT(userdata);
    gint ret = 0;

    switch (sortcol)
    {
      case SORTID_NAME:
      {
        gchar *name1, *name2;

        gtk_tree_model_get(model, a, COL_NAME, &name1, -1);
        gtk_tree_model_get(model, b, COL_NAME, &name2, -1);

        if (name1 == NULL || name2 == NULL)
        {
          if (name1 == NULL && name2 == NULL)
            break; /* both equal => ret = 0 */

          ret = (name1 == NULL) ? -1 : 1;
        }
        else
        {
          ret = g_utf8_collate(name1,name2);
        }

        g_free(name1);
        g_free(name2);
      }
      break;

      case SORTID_YEAR:
      {
        guint year1, year2;

        gtk_tree_model_get(model, a, COL_YEAR_BORN, &year1, -1);
        gtk_tree_model_get(model, b, COL_YEAR_BORN, &year2, -1);

        if (year1 != year2)
        {
          ret = (year1 > year2) ? 1 : -1;
        }
        /* else both equal => ret = 0 */
      }
      break;

      default:
        g_return_val_if_reached(0);
    }

    return ret;
  }


  void
  create_list_and_view (void)
  {
    GtkTreeSortable *sortable;

    ...

    liststore = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_UINT);

    sortable = GTK_TREE_SORTABLE(liststore);

    gtk_tree_sortable_set_sort_func(sortable, SORTID_NAME, sort_iter_compare_func,
                                    GINT_TO_POINTER(SORTID_NAME), NULL);

    gtk_tree_sortable_set_sort_func(sortable, SORTID_YEAR, sort_iter_compare_func,
                                    GINT_TO_POINTER(SORTID_YEAR), NULL);

    /* set initial sort order */
    gtk_tree_sortable_set_sort_column_id(sortable, SORTID_NAME, GTK_SORT_ASCENDING);

    ...

    view = gtk_tree_view_new_with_model(liststore);

    ...

  }


通常,如果你利用树视图列标题进行排序,事情会更容易一些,在这种情况下,你只需要分配排序列 ID 和你的比较函数,但不需要自己设置当前排序列 ID 或顺序(见下文)。

你的树迭代器比较函数应该返回一个负值,如果由迭代器 a 指定的行在由迭代器 b 指定的行之前,并且返回一个正值,如果行 b 在行 a 之前。如果两个行根据你的排序标准相等,它应该返回 0(你可能希望使用第二个排序标准来避免当存储被重新排序时相等行的 '跳跃')。你的树迭代器比较函数不应考虑排序顺序,而是假设升序排序(否则会发生不好的事情)。

GtkTreeModelSort

[编辑 | 编辑源代码]

GtkTreeModelSort 是一个包装树模型。它采用另一个树模型,如列表存储或树存储作为子模型,并将子模型以排序状态呈现给 '外部'(即树视图或任何其他通过树模型接口访问它的人)。它在不改变子模型中行顺序的情况下做到这一点。如果你想在不同的树视图中以不同的排序标准显示相同的模型,或者如果你需要在某个时刻恢复存储的原始未排序状态,这将很有用。

GtkTreeModelSort 实现了 GtkTreeSortable 接口,因此你可以像对待你的数据存储一样对待它以进行排序目的。以下是带有树视图的基本设置

  ...

  void
  create_list_and_view (void)
  {
    ...

    liststore = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_UINT);

    sortmodel = gtk_tree_model_sort_new_with_model(liststore);

    gtk_tree_sortable_set_sort_func(GTK_TREE_SORTABLE(sortmodel), SORTID_NAME,
                                    sort_func, GINT_TO_POINTER(SORTID_NAME), NULL);

    gtk_tree_sortable_set_sort_func(GTK_TREE_SORTABLE(sortmodel), SORTID_YEAR,
                                    sort_func, GINT_TO_POINTER(SORTID_YEAR), NULL);

    /* set initial sort order */
    gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(sortmodel),
                                         SORTID_NAME, GTK_SORT_ASCENDING);

    ...

    view = gtk_tree_view_new_with_model(sortmodel);

    ...

  }

  ...


但是,当使用排序树模型时,在使用模型的迭代器和路径时,你需要小心。这是因为指向视图(以及这里的排序树模型)中行的路径可能不会指向子模型(你的原始列表存储或树存储)中的同一行,因为子模型中的行顺序可能与排序顺序不同。类似地,对排序树模型有效的迭代器对子模型无效,反之亦然。你可以使用 gtk_tree_model_sort_convert_child_path_to_path、gtk_tree_model_sort_convert_child_iter_to_iter、gtk_tree_model_sort_convert_path_to_child_path 和 gtk_tree_model_sort_convert_iter_to_child_iter 将路径和迭代器从子模型转换为子模型。你不太可能经常需要这些函数,因为你仍然可以使用 gtk_tree_model_get 直接在排序树模型上使用树视图提供的路径。

对于树视图,排序树模型是 '真实' 模型 - 它根本不知道排序树模型的子模型,这意味着你从树视图在回调中或其他地方接收的任何路径或迭代器都将引用排序树模型,并且如果你调用树视图函数,你需要传递引用排序树模型的路径或迭代器。

排序和树视图列标题

[编辑 | 编辑源代码]

除非你隐藏了树视图列标题或使用自定义树视图列标题小部件,否则每个树视图列的标题都可以点击。然后,点击树视图列的标题将根据该列中的数据对列表进行排序。你需要做两件事来实现这一点:首先,你需要使用 gtk_tree_sortable_set_sort_func 告诉你的模型对哪个排序列 ID 使用哪个排序函数。完成此操作后,你告诉每个树视图列,如果点击此列的标题,哪个排序列 ID 应该处于活动状态。这是使用 gtk_tree_view_column_set_sort_column_id 完成的。

实际上,这就是你让列表或树排序所需的全部操作。如果你点击列标题,树视图列将自动为你设置活动排序列 ID 和排序顺序。

不区分大小写的字符串比较

[编辑 | 编辑源代码]

如上所述,在“GtkCellRendererText、UTF8 和 pango 标记”部分中,所有要在树视图中显示的字符串都需要用 UTF8 编码进行编码。所有 ASCII 字符串都是有效的 UTF8,但是一旦使用非 ASCII 字符,事情就会变得有点棘手,字符编码就会很重要。

忽略大小写比较两个 ASCII 字符串非常简单,可以使用 g_ascii_strcasecmp 完成,例如。strcasecmp 通常会做同样的事情,只是它在某种程度上也是语言环境感知的。唯一的问题是,许多用户使用不是 UTF8 的语言环境字符编码,因此 strcasecmp 对我们没有多大帮助。

g_utf8_collate 将比较 UTF8 编码中的两个字符串,但它不会忽略大小写。为了实现至少半途而废的正确的语言学不区分大小写的排序,我们需要采取两步法。例如,我们可以使用 g_utf8_casefold 将要比较的字符串转换为独立于大小写的形式,然后使用 g_utf8_collate 比较这两个字符串(请注意,g_utf8_casefold 返回的字符串不会以任何可识别的形式类似于原始字符串;它们将适用于比较)。或者,可以使用 g_utf8_strdown 对两个字符串进行操作,然后使用 g_utf8_collate 再次比较结果。

显然,所有这些都不会很快,如果你有很多行,就会累积起来。为了加快速度,你可以使用 g_utf8_collate_key 创建一个 '整理键',并将其存储在你的模型中。整理键只是一个对我们毫无意义的字符串,但可以与 strcmp 一起用于字符串比较目的(比 g_utf8_collate 快得多)。

需要注意的是,g_utf8_collate 排序的方式取决于当前语言环境。在你对奇怪的排序顺序感到困惑之前,请确保你不在 'C' 语言环境(=默认,未指定)中工作。在命令行上使用 'echo $LANG' 检查你当前的语言环境设置。

查看 GLib API 参考中的“Unicode 操作”部分以获取更多详细信息。

华夏公益教科书