关于GtkTreeView和 MVC的一篇好文章 入木三分

http://rat.nutn.edu.tw/~slayer/myarticle/gtk_tree_view_tutorial.html

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來協助。

  • 我們先來看GtkTreeView與GtkTreeModel的部分。上面虛線的部分是已經封裝於內部的行為,Model要通知View更新自己,View透過Controller改變Model。這裡我畫了一個想像的類別ImaginaryGtkController,是代表了所有 GtkTreeModel"應有的"行為的集合。一般來說,Controller是以Strategy Design Pattern(以下簡稱DP)的方式設計的。別看到DP就被嚇到了,只是把前人已經有的經驗各別取上一些名字集結而成Pattern。有些東西是我們習以為常的,有些可能沒用過,但卻深入於各家套件的設計哲學之上。Strategy主要的精神就是區隔變動與不變之間,我們透過一組方法來操作Model,這一組方法就是我們的Controller。但是我們並沒有以建立一個Controller類別為出發的觀點來想這件而,而是先定義好操作Model應該有的行為,將每一個行為獨立出來成為未實作的介面。不同的Model由不同的Controller操作,但都是使用同樣規格的方法,實作上卻又是不同的。實作上可以抽換,保持架構上的彈性。但是,Gtk是以c寫成的。非真正支援OO的語言,要達到相似的概念又要一番功夫。所以,在這部分我畫上想像的引、已經實作了所有相關行為的Controller類別。也許你想問個明白,這Controller到底是我們將使用的那些函式呢?這裡簡單舉二個: gtk_list_store_append、gtk_list_store_set,皆被付予操作Model內容的資格,故為Controller。
  • 以View的部分來說,對於使用library的人另一個覺得麻煩之處在於View的處理不只是GtkTreeView在控制,還要與 GtkTreeViewColumn及各種GtkCellRenderer,他們是依順序一個包含著一個。注意! 我這裡提到了包含,請配合聯想為"容器"。GtkTreeView是裝GtkTreeViewColumn的容器,GtkTreeViewColumn是 GtkCellRenderer的容器。不過他們置入的"動詞"不太一樣。圖中只簡單舉了一二個;GtkTreeView以append的方式加入新的 column,GtkTreeViewColumn以pack的方式加入新的cell renderer。最後,您也許會想View都搞定了怎麼把Model放進來,再回頭看看圖上有寫,gtk_tree_view_set_model就是他了。
  • 在講完比較大的概念後還有一點細節的部分要提醒的 -- "detail is ghost"。這關於GObject的記憶體管理模式,本篇文件不打算著墨於此,只簡單提醒您有部分物件的使用您必需自行呼叫 g_object_unref一次,讓這一個物件參考到的"參照"計數減一。GObject有自己的garbage collection機制來回收不再使用的記憶體,在此只簡單地說"當一個GObject不再被其他物件引用到時(也就是reference count為0)他會自動被回收。而我們在使用GtkTreeView相關的元件與物件時,有部分需要去關心一下他是不是會達到自動回收的標準,但是這裡又沒有什麼規範請各位準備好小抄:)
    在Model方面,GtkTreeModel與他的衍生類別GtkTreeStore、GtkListStore是需要g_object_unref處理的;在View方面GtkCellRendererPixbuf也是需要做這樣的處理。

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

  • 我們新增了一個GtkTreeView不過我們還沒有設定他的內容,所以看起來一片空白。
  • 接著,你可以選擇先搞定View或是先準備好Model。不論誰先都應該要先想好每一個欄位的名稱及資料型態。

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

  • 在這裡我們還沒真的新增一個GtkListStore類別,而是先新增了建立GtkListStore的函式及以新增資料到 GtkListStore的函式。僅管Gtk在設計上是個不錯的library,但在這一部分要直接拿來用上有點算太過麻煩。建議上是包裝成自己需要的功能,直接使用的方式怎麼想都太原始,除非您寫的是library否則一般的應用程式這樣使用就足夠了。
  • 我們要建立GtkListStore需要幾個訊息,"打算用幾個欄位來存放資料"、"每一個欄位的型態是什麼"。這些資料恰巧為gtk_list_store_new ()所需的參數。第一個參數是所需要的欄數,在程式中您會發現我填的是一個enum中的值。這裡的enum中有許多tag是給 GtkListStore的Controller函式用的,當你新增一筆(一列)資料後要指定每一欄的值,而欄位的標號是由0啟始與陣列相仿。利用 enum自動累加的特性,將第一個tag設值為0直到最後一個tag(欄位名稱),再多加一個tag即方便的得到了欄位的數量。 gtk_list_store_new ()剩下的參數你會發現是一個變動長度的引數列,因為無法預估使用者需要多少個欄位所以您會常在這一系列相關的函式發現這樣的寫法。回到我們的問題,接下來要填的是欄位的資料型態。這裡要填寫的是GType,常用的如G_TYPE_STRING(字串資料)、G_TYPE_INT(數值資料)、 G_TYPE_PIXBUF(圖象),想深入瞭解的讀著建議閱讀GTK+ 2.0 Tree View Tutorial。在這裡我們配合了規劃好的欄位分別填上了G_TYPE_STRING、G_TYPE_STRING、 G_TYPE_UINT。最後,我們在create_model函式回傳時把GtkListStore cast成為GtkTreeModel。
  • 在建立Model之後,我們要透過Iterator來對資料做操作,這裡示範了簡單新增的方法。對於Model來說,我們要新增資料要做二件事,一為新增一列資料空間做為存放資料而準備;一旦有了空間能存放資料,我們就可以把資料指定到各個欄位裡。新增空間的動作為 gtk_list_store_append,他需要Model的指標與Iterator指標。新增空間之後能用gtk_list_store_set來指定該列資料的內容,除了與gtk_list_stroe_append中一樣的參數外,當然還需要欄位的位置與內容,欄位位置就是之前由enum所設定的那些。最後,要加上-1表示資料已經沒有資料要新增了。

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


  • 我們新增了一個arrange_tree_view函式,其實只是把新增column的工作交給他罷了。而我們這裡偷懶使用了gtk_tree_view_column_new_with_attributes ()函式來新增column,這個函式其實就是幫我們呼叫了gtk_tree_view_column_new()、 gtk_tree_view_column_set_title()、gtk_tree_view_column_pack_start()、 gtk_tree_view_column_set_attributes(),最後因為GtkTreeViewColumn是一個GObject,所以我們如果不是使用gtk_tree_view_column_new_with_attributes那產生出來的GObject在交付給 GtkTreeView後請記得使用g_object_unref()。
  • gtk_tree_view_column_new_with_attributes中所要設定的"屬性"是給 GtkCellRenderer使用的,以我們的例子來講,我們希望最後顯在表格中的是文字資料,所以設定為"text"。(? 有沒有人知這要去那查啊orz)(關於Attribute的詳細解說可參閱Gtk2.0+ Tree View Tutorial 5.2 Attributes)
  • 到目前為止,我們已經把Model和View的基礎建立都寫好了,接著到main function中來組裝他們吧! 首先,我們以arrange_tree_view函式放入準備好的GtkTreeViewColumn,再以create_model函式建立好 Model。再來使用gtk_tree_view_set_model建立起GtkTreeView與GtkTreeModel之間的聯繫。最後,別忘了對Model呼叫g_object_unref。

Endless


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

你可能感兴趣的:(treeview)