跳轉到內容

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(待辦事項)

[編輯 | 編輯原始碼]

未完成的章節。

華夏公益教科書