Author: qrtt1 2006/07/11

Preface


想要使用GtkTreeView實在不是一件"簡單"的事。我在這把簡單特意括了起來,是因為要提醒您一下。我並不是想要暗示您聯想到他是很難的, 在這裡我選擇了另一種相對的意義 -- 繁複。步驟多了一點,但概念上並不算難以理解。也許您已經領教過落落長的GTK+ 2.0 Tree View Tutorial(Tim-Philipp Mler, 2005)作者是希望他能涵蓋大部分的主題,所以篇幅與細微的程度當然是有所要求的。不過每個讀者都篇好不同的風格,弟就試著寫一篇具體而微的短篇試試。


需要有MVC的概念嗎?


在吸收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)


Model/View/Controller in GtkTree* groups


在前一個段落介紹了點MVC的概念,實在得承認這是很偷懶的介紹方式。MVC實在是一個很大的議題啊! 暫且先隨我"短視"一下,我們來看一下GtkTreeView、GtkTreeViewColumn、GtkTreeModel、 GtkCellRenderer、GtkTreeIter分別代表MVC的那些部分。先來看一下下面這一張"簡化"的類別圖。GtkTreeView是整 個Widget的門面,只有GtkTreeView並不能真的讓我們的程式有用,還需要GtkTreeModel的協助才能夠持有資料。而每一種資料的呈 現方式也不盡相同,所以還需要GtkCellRenderer來協助。

uml for gtk tree group overview

A example based on GtkListStore


使 用GtkTreeView上的手續也許有點繁複,但也就是那幾件事為View建立GtkTreeView、GtkTreeViewColumn、 GtkCellRenderer;為Model建立GtkListStore或GtkTreeStore。最後,用 gtk_tree_view_set_model讓他們相連在一起。

gtk application sketch

#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;
}

add GtkTreeView widget

#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;
}
only gtk tree view

prepare `Model`

在這裡我們示範如何使用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;
}


add GtkTreeViewColumn and
GtkCellRenderer

#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;
}

tree view

Endless


雖然文件只寫到這裡,但是才是您Gtk Tree View學習的起點。在這裡只是先讓您撇開複雜的部分,先體驗一下使用上的workflow。先掌握了流程,再針對各流程的細節去了解。而不是一開始就挖 向細微的部分,見樹不見林。從此怕害而停滯不前:)