想要使用GtkTreeView實在不是一件"簡單"的事。我在這把簡單特意括了起來,是因為要提醒您一下。我並不是想要暗示您聯想到他是很難的,在這裡我選擇了另一種相對的意義 -- 繁複。步驟多了一點,但概念上並不算難以理解。也許您已經領教過落落長的GTK+ 2.0 Tree View Tutorial (Tim-Philipp Mler, 2005)作者是希望他能涵蓋大部分的主題,所以篇幅與細微的程度當然是有所要求的。不過每個讀者都篇好不同的風格,弟就試著寫一篇具體而微的短篇試試。
在吸收GTK+ 2.0 Tree View Tutorial的同時,也複習了一下MVC這一個複合式的design pattern。反覆思考著,學習GtkTreeView的使用,真的需要有MVC的概念嗎? 雖然Gtk這一個GUI的library內一定用了許MVC的設計思維,但是對於使用者來說我們不一定明白MVC著力於那些地方,一直拿出來強調反而使人 困惑。但當你要學習使用GtkTreeView時,您就不能夠再將MVC視而不見了,因為這是一個MVC的半成品。就像在玩猜字遊戲般,您要在對的格子填 上有用的資訊,縱橫交錯之下才能使整個遊戲完美了起來。
在這裡,我不打算深入解釋MVC。但是要先為MVC這三個字母,做一下"狹義"的定義;M - Model,你可以把他當成"資料",並且有一組專用的函式負責操作(增加/刪除/排序/查詢等功能)這些"資料";V - View,在Gtk中你可以想像成Widgets,任何可以把"資料"顯示給終於使用者的東西,都可以稱為View。C - Controller,這裡採用比較不精確的講法 -- 協調者。負著協調View與Model應用的反應。
稍作名詞定義了之後,我們來稍懂一下MVC的互動模式。這有點像三角關係,但又如同編劇為各個角色設定了令人扼腕的個性。View總是優柔寡斷沒有 自己的意見把Model的想法當成是自己的想法,Model總是自我中心要整個世界跟他著轉動,Controller是唯一讓Model信任的朋 友,Model 只肯為了Controller做出改變,而Controller與View的關係也是唯妙的,View是唯一能讓Controller做點什麼的人。他們 一個看著一個的背影,視線只在二者之間。
故事聽完之後,我們回到嚴肅一點的情境。Model也就是程式所要操弄的資料,沒有資料程式就不具有存在的意義。但是我們要寫GUI程式,左一個 button右一個button很容易不小心就觸動了什麼,萬一這一個觸發可以直接改變Model,但是GUI上對應Model狀態的元件卻沒改變就變成 了dirty data,當然你也可以選則在更動的同時更新顯示狀態的元件。但是這樣並不理想,萬一這些元件的值需要與其他未更動的資料交互運算,這樣程式的複雜性就增 加了。GUI(View)與Model較緊密地結合在一起,實在不是一個理想的設計。
在這裡,需要知道Model是否被改變了,而View要也為改變做出反應。前人們就思考著除了不斷地在背景查詢Model是不是真的改變了,再來更 新 View這種笨拙的方式時。想出了另一種設計思維"Don't Call Me, I'will Call You"[1]。Model對View說,別找我,有事我會找你。這樣主動的角色就調換了,讓Model主動通知View,他的狀態已經有所改變了。對 View來說,他自從不主動之後。生活上有點改變了。變得悠閒了,沒事不會去找事做。為了這樣的改變提供公用的update函式,並且把自己登記在 Model的通知名冊之上,讓Model在狀態改變時可以通知他update。(M與V)
剛剛提到了"資料被改變",回頭想想改變的起點。不就是做在電腦前的各位使用者嗎?你正享受著GUI程式,上面也許有許多按鈕,也許有地方讓你寫點 什麼抒發一下情感。這任何一個動作都可能造成Model有所改變。但是以MVC的思考模式,View並不會直接改變Model,而是向 Controller請求改變,透過Controller去改變Model。而Controller通常是一組對應View中所提供的功能的函式,代表著 View中應有的行為。當然有些行為並不會改變Model。(V與C)
在前一個段落介紹了點MVC的概念,實在得承認這是很偷懶的介紹方式。MVC實在是一個很大的議題啊! 暫且先隨我"短視"一下,我們來看一下GtkTreeView、GtkTreeViewColumn、GtkTreeModel、 GtkCellRenderer、GtkTreeIter分別代表MVC的那些部分。先來看一下下面這一張"簡化"的類別圖。GtkTreeView是整 個Widget的門面,只有GtkTreeView並不能真的讓我們的程式有用,還需要GtkTreeModel的協助才能夠持有資料。而每一種資料的呈 現方式也不盡相同,所以還需要GtkCellRenderer來協助。
使用GtkTreeView上的手續也許有點繁複,但也就是那幾件事為View建 立GtkTreeView、GtkTreeViewColumn、 GtkCellRenderer;為Model建立GtkListStore或GtkTreeStore。最後,用 gtk_tree_view_set_model讓他們相連在一起。
#include <gtk/gtk.h> #include <glib.h> int main( int argc, char *argv[] ) { /* GtkWidget is the storage type for widgets */ GtkWidget * window; /* This is called in all GTK applications. Arguments are parsed * from the command line and are returned to the application. */ gtk_init ( &argc, &argv ); window = gtk_window_new( GTK_WINDOW_TOPLEVEL ); gtk_window_set_title( GTK_WINDOW( window ), "Tree"); g_signal_connect( G_OBJECT( window ), "destroy", gtk_main_quit, NULL); gtk_widget_show_all ( window ); /* All GTK applications must have a gtk_main(). Control ends here * and waits for an event to occur (like a key press or * mouse event). */ gtk_main (); return 0; }
#include <gtk/gtk.h> #include <glib.h> int main( int argc, char *argv[] ) { /* GtkWidget is the storage type for widgets */ GtkWidget * window; /* This is called in all GTK applications. Arguments are parsed * from the command line and are returned to the application. */ gtk_init ( &argc, &argv ); window = gtk_window_new( GTK_WINDOW_TOPLEVEL ); gtk_window_set_title( GTK_WINDOW( window ), "Tree"); g_signal_connect( G_OBJECT( window ), "destroy", gtk_main_quit, NULL); GtkWidget * view;
view = gtk_tree_view_new();
gtk_container_add( GTK_CONTAINER(window), view); gtk_widget_show_all ( window ); /* All GTK applications must have a gtk_main(). Control ends here * and waits for an event to occur (like a key press or * mouse event). */ gtk_main (); return 0; }
在這裡我們示範如何使用GtkListStore這個GtkTreeModel的子類別,GtkTreeStore的用法也大同小異,只不過他包含了樹狀這一種的階層關係,所以在資料上的檢索方式不太一樣,所以對Iterator的走訪方式也設計了不同的用法。
#include <gtk/gtk.h> #include <glib.h>
enum{
col_name = 0,
col_date,
col_size,
n_cols
};
void model_data_new(GtkTreeModel* store,
const gchar* name, const gchar* date, const guint size) {
GtkTreeIter iter;
gtk_list_store_append(GTK_LIST_STORE(store), &iter);
gtk_list_store_set(GTK_LIST_STORE(store), &iter,
col_name, name,
col_date, date,
col_size, size,
-1);
}
GtkTreeModel* create_model() {
GtkListStore *store;
store = gtk_list_store_new (n_cols,
G_TYPE_STRING,G_TYPE_STRING,G_TYPE_UINT);
return GTK_TREE_MODEL(store);
} int main( int argc, char *argv[] ) { /* GtkWidget is the storage type for widgets */ GtkWidget * window; /* This is called in all GTK applications. Arguments are parsed * from the command line and are returned to the application. */ gtk_init ( &argc, &argv ); window = gtk_window_new( GTK_WINDOW_TOPLEVEL ); gtk_window_set_title( GTK_WINDOW( window ), "Tree"); g_signal_connect( G_OBJECT( window ), "destroy", gtk_main_quit, NULL); GtkWidget * view; view = gtk_tree_view_new(); gtk_container_add( GTK_CONTAINER(window), view); gtk_widget_show_all ( window ); /* All GTK applications must have a gtk_main(). Control ends here * and waits for an event to occur (like a key press or * mouse event). */ gtk_main (); return 0; }
#include <gtk/gtk.h> #include <glib.h> enum{ col_name = 0, col_date, col_size, n_cols }; void model_data_new(GtkTreeModel* store, const gchar* name, const gchar* date, const guint size) { GtkTreeIter iter; gtk_list_store_append(GTK_LIST_STORE(store), &iter); gtk_list_store_set(GTK_LIST_STORE(store), &iter, col_name, name, col_date, date, col_size, size, -1); } GtkTreeModel* create_model() { GtkListStore *store; store = gtk_list_store_new (n_cols, G_TYPE_STRING,G_TYPE_STRING,G_TYPE_UINT); return GTK_TREE_MODEL(store); } void arrange_tree_view(GtkWidget* view) {
GtkCellRenderer* renderer;
// col 1: name
renderer = gtk_cell_renderer_text_new ();
gtk_tree_view_insert_column_with_attributes(
GTK_TREE_VIEW(view), -1, "name", renderer, "text", col_name, NULL);
// col 2: date
gtk_tree_view_insert_column_with_attributes(
GTK_TREE_VIEW(view), -1, "date", renderer, "text", col_date, NULL);
// col 3: size
gtk_tree_view_insert_column_with_attributes(
GTK_TREE_VIEW(view), -1, "size", renderer, "text", col_size, NULL);
}
int main( int argc, char *argv[] ) { /* GtkWidget is the storage type for widgets */ GtkWidget * window; /* This is called in all GTK applications. Arguments are parsed * from the command line and are returned to the application. */ gtk_init ( &argc, &argv ); window = gtk_window_new( GTK_WINDOW_TOPLEVEL ); gtk_window_set_title( GTK_WINDOW( window ), "test"); g_signal_connect( G_OBJECT( window ), "destroy", gtk_main_quit, NULL); GtkWidget* view; view = gtk_tree_view_new(); gtk_container_add( GTK_CONTAINER(window), view); // arrange view columns
arrange_tree_view(view);
// set model
GtkTreeModel* store = create_model();
gtk_tree_view_set_model ( GTK_TREE_VIEW(view), store);
model_data_new(store, "test.c", "2006-07-29", 2224);
model_data_new(store, "xd.c", "2006-07-29", 454);
g_object_unref( store );
gtk_widget_show_all ( window ); /* All GTK applications must have a gtk_main(). Control ends here * and waits for an event to occur (like a key press or * mouse event). */ gtk_main (); return 0; }
雖然文件只寫到這裡,但是才是您Gtk Tree View學習的起點。在這裡只是先讓您撇開複雜的部分,先體驗一下使用上的workflow。先掌握了流程,再針對各流程的細節去了解。而不是一開始就挖向細微的部分,見樹不見林。從此怕害而停滯不前:)