跳轉到內容

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 操作”部分以獲取更多詳細資訊。

華夏公益教科書