FoundationsofGTKDevelopment:Chapter 08

From 흡혈양파의 번역工房
Jump to: navigation, search
제 8 장 트리 뷰 위젯

트리 뷰 위젯

이번 장에서는 GtkScrolledWindow 위젯을 GtkTreeView라고 알려진 또 다른 강력한 위젯과 함께 사용하는 방법을 보이고자 한다. 트리 뷰 위젯은 하나 또는 그 이상의 열에 이어지는 리스트나 트리에 데이터를 표시하는 데 사용된다. 가령 GtkTreeView를 이용해 통합 개발 환경의 결과를 표시하거나 파일 브라우저를 구현하는 것이 가능하다.


GtkTreeView는 다양한 기능을 제공하기 때문에 관련된 위젯이므로 이번 장의 각 절을 유심히 읽도록 한다. 이 강력한 위젯에 대해 학습하고 나면 많은 애플리케이션에서 적용할 수 있을 것이다.


이번 장은 GtkTreeView가 제공하는 수많은 기능을 소개할 것이다. 본 장에 제시된 정보는 자신의 요구를 충족시키도록 자신만의 트리 뷰 위젯을 만들기에 충분하다. 그 중에서도 다음 내용을 집중적으로 학습할 것이다.

  • GtkTreeView를 생성하는 데 사용되는 객체가 무엇이며 그 모델-뷰-컨트롤러 디자인이 객체를 유일하게 만드는 방법
  • GtkTreeView 위젯을 이용해 리스트와 트리 구조를 생성하는 방법
  • GtkTreeView 내에서 행을 참조 시 GtkTreePath, GtkTreeIter, 또는 GtkTreeRowReference를 이용해야 하는 시기
  • 더블 클릭, 단일 행 선택, 다중 행 선택을 처리하는 방법
  • 편집 가능한 트리 셀을 생성하거나 셀 렌더러(cell renderer) 함수를 이용해 각 셀을 맞춤설정하는 방법
  • 토글 버튼, pixbuf, 스핀 버튼, 콤보 박스, 진행 막대, 키보드 가속기 문자열을 포함해 셀에 포함시킬 수 있는 위젯


트리 뷰의 부분들

GtkTreeView 위젯은 데이터를 리스트나 트리로 구조화하여 표시할 때 사용된다. 데이터는 뷰에서 행과 열로 표시된다. 사용자는 마우스나 키보드를 이용해 트리 뷰에서 하나 또는 여러 개의 행을 선택할 수가 있다. GtkTreeView를 이용한 Nautilus 애플리케이션을 그림 8-1에서 확인할 수 있다.

그림 8-1. GtkTreeView 위젯을 이용한 Nautilus


GtkTreeView는 사용하기 힘들고 이해하기는 더더욱 힘든 위젯이므로 이번 장에서는 그 사용을 집중적으로 살펴보고자 한다. 하지만 위젯이 어떻게 작동하는지 우선 이해하고 나면 위젯이 사용자에게 표시되는 방식에 관한 거의 모든 측면은 맞춤설정이 가능하므로 광범위한 애플리케이션에 적용할 수 있을 것이다.


GtkTreeView가 독특한 이유는 일반적으로 모델-뷰-컨트롤러(MVC) 디자인에 해당하는 디자인 개념을 따르기 때문이다. MVC는 정보와 정보가 렌더링되는 방식이 완전히 독립적으로 이루어지는 디자인 방식으로, GtkTextView와 GtkTextBuffer 간 관계와 비슷하다.


GtkTreeModel

데이터 자체는 GtkTreeModel 인터페이스를 구현하는 클래스에 저장된다. GTK+는 네 가지 타입의 내장된 트리 모델 클래스를 제공하지만 이번 장에서는 GtkListStore와 GtkTreeStore만 다룰 것이다.


GtkTreeModel 인터페이스는 데이터가 저장되는 것과 관련된 일반적인 정보를 검색하는 데 사용할 수 있는 표준 메서드 집합을 제공한다. 가령 이것은 트리 내 열의 개수나 특정 행에 위치한 자식 개수를 알아내도록 해준다. GtkTreeModel은 저장소(store)의 특정 행에 저장된 데이터를 검색하는 방법을 제공하기도 한다.


Gtkd note.png 모델, 렌더러, 열은 GTK+ 라이브러리에 속하긴 하지만 위젯이 아니라 객체로 지칭된다. 이러한 구별이 특히 중요한 이유는, 이들이 GtkWidget에서 파생되지 않아 GTK+ 위젯에서 이용할 수 있는 것과 동일한 함수, 프로퍼티, 시그널을 갖고 있지 않기 때문이다.


GtkListStore는 다중 열로 된 요소 리스트를 생성하도록 해준다. 각 행은 루트 노드의 자식이므로 하나의 행 수준만 표시된다. 기본적으로 GtkListStore는 상속구조가 없는 트리 구조체다. 이는 자식 항목이 없는 모델과의 상호작용을 위해 속도가 더 빠른 알고리즘이 존재하기 때문에 제공할 뿐이다.


GtkTreeStore는 데이터가 다중 레이어로 된 트리로 구성될 수 있다는 점만 제외하면 GtkListStore와 비슷한 기능을 제공한다. GTK+는 자신만의 커스텀 모델 타입을 생성하는 방법도 제공하지만 대부분의 경우 GTK+에서 제공하는 두 가지 타입만으로 충분할 것이다.


GtkListStore와 GtkTreeStore는 대부분의 애플리케이션에 충분하지만 자신만의 저장소(store) 객체를 구현해야 하는 경우가 종종 있다. 예를 들어, 보유해야 할 행의 수가 많다면 좀 더 효율적인 새 모델을 생성해야 할 것이다. 제 11장에서는 GObject에서 파생시켜 새로운 클래스를 생성하는 방법을 학습할 것인데, 그 내용을 지침서로 삼아 GtkTreeModel 인터페이스를 구현하는 새로운 클래스를 파생해보길 권한다.


트리 모델을 생성한 다음에는 뷰를 이용해 데이터를 표시한다. 트리 뷰와 그 모델을 구분함으로써 동일한 데이터 집합을 다중 뷰에 표시할 수 있다. 이러한 뷰들 서로가 정확히 동일한 복사본이 되기도 하고, 데이터가 다양한 방식으로 표시되기도 한다. 개발자가 모델을 수정하면 모든 뷰는 동시적으로 업데이트될 것이다.


Gtkd tip.png 동일한 데이터 집합을 다중 트리 뷰에 표시하는 것이 그 당시엔 그다지 유용하지 않아 보일지도 모르지만 파일 브라우저의 경우를 고려해보라. 다수의 파일 브라우저에 동일한 파일 집합을 표시해야 할 경우, 각 뷰에 동일한 모델을 이용한다면 메모리를 절약할 뿐만 아니라 프로그램의 실행 속도로 상당히 빨라질 것이다. 이는 파일 브라우저에 여러 가지의 디스플레이 옵션을 제공하고자 할 때도 유용하다. 디스플레이 모드 간 전환 시 데이터 자체를 수정하지 않아도 될 것이다.


모델은 동일한 데이터 타입을 포함하는 열과 각 데이터 집합을 포함하는 행으로 구성된다. 각 모델 열은 하나의 데이터 유형을 보유할 수 있다. 트리 모델 열과 트리 뷰 열을 혼동해선 안 되는 것이, 트리 뷰 열은 하나의 헤더로 구성되지만 다수의 모델 열로부터 데이터를 렌더링할 수도 있다. 가령 트리 열은 사용자에게 보이지 않는 모델 열에서 정의된 전경색으로 된 텍스트 문자열을 표시할 수도 있다. 그림 8-2에 모델 열과 트리 열의 차이를 표시하였다.

그림 8-2. 모델 열과 트리 열의 관계


모델 내의 각 행은 각 모델 열에 해당하는 데이터를 한 조각씩 포함한다. 그림 8-2에서 각 행은 텍스트 문자열과 GdkColor 값을 포함한다. 이 두 값은 트리 열에 해당하는 색상으로 된 텍스트를 표시하는 데 사용된다. 이를 코드에서 구현하는 방법은 뒷부분에서 살펴볼 것이다. 지금은 두 가지 타입의 열을 구별하고 그들이 어떻게 관계되는지를 이해하는 것으로 충분하겠다.


새로운 리스트와 트리 저장소(tree store)는 열의 개수를 이용해 생성되는데, 각 열은 이미 존재하는 GType에 의해 정의된다. 보통은 GLib에 이미 구현된 것만으로 충분할 것이다. 예를 들어 텍스트를 표시하길 원한다면 G_TYPE_STRING, G_TYPE_BOOLEAN을 이용하거나 G_TYPE_INIT와 같은 타입을 몇 가지 이용할 수 있다.


Gtkd tip.png G_TYPE_POINTER를 이용하면 임의의 데이터 유형을 저장하는 것이 가능하므로 각 행에 관한 정보를 저장하려면 하나 또는 그 이상의 트리 모델 열을 이용하면 된다. 행의 개수가 많으면 메모리 사용이 급격히 증가하므로 주의를 기울여야 한다. 포인터를 해제하는 일 또한 개발자의 책임이다.


GtkTreeViewColumn과 GtkCellRenderer

앞서 언급하였듯 트리 뷰는 하나 또는 그 이상의 GtkTreeViewColumn 객체를 표시한다. 트리 열은 헤더와 데이터의 셀로 구성되는데, 데이터의 셀은 하나의 열에 조직된다. 또한 각 트리 뷰 열은 눈에 보이는 데이터 열을 하나 또는 그 이상 포함한다. 예를 들어, 파일 브라우저에서 트리 뷰 열은 하나의 이미지 열과 하나의 파일명 열을 포함할 수 있겠다.


GtkTreeViewColumn 위젯의 헤더는 아래 셀에 어떤 데이터가 있는지 설명하는 제목을 포함한다. 열을 정렬가능(sortable)하게 만들었다면 열 헤더 중 하나를 클릭 시 행이 정렬될 것이다.


트리 뷰 열은 사실 어떤 것도 화면으로 렌더링하지 않는다. 이는 GtkCellRenderer로부터 파생된 객체를 이용해 이루어진다. 셀 렌더러는 트리 뷰 열로 패킹되는데, 이는 위젯을 수평 박스로 패킹할 때와 유사한 방식으로 이루어진다. 각 트리 뷰 열은 하나 또는 이상의 셀 렌더러를 포함할 수 있으며 이는 데이터의 렌더링에 사용된다. 가령 파일 브라우저에서 이미지 열은 GtkCellRendererPixbuf를 이용해 렌더링될 것이고 파일명은 GtkCellRendererText를 이용해 렌더링될 것이다. 이 예를 그림 8-1에 표시하였다.


각 셀 렌더러는 셀의 열을 렌더링하는 작업을 책임지는데, 트리 뷰의 행마다 한 번씩 이루어진다. 첫 번째 행부터 시작되어 그 셀을 렌더링하고 다음 행으로 진행되는 식으로 전체 열 또는 열의 일부가 렌더링될 때까지 지속된다.


셀 렌더러는 각 데이터 셀이 화면으로 렌더링되는 방식을 정의하는 프로퍼티들로 구성된다. 셀 렌더러 프로퍼티를 설정하는 방법에는 다수가 있다. 가장 쉬운 방법은 g_object_set()을 이용하는 방법으로, 셀 렌더러가 실행 중인 열에 있는 모든 셀로 설정을 적용할 것이다. 이는 속도가 매우 빠르지만 개발자가 특정 셀에 대한 속성을 설정해야 하는 경우가 종종 있다.


그 외에 렌더러에 속성(attribute)을 추가하는 방법이 있다. 열 속성은 트리 모델 열에 상응하며, 그림 8-3에서 볼 수 있듯이 셀 렌더러 프로퍼티와 연관된다. 이러한 프로퍼티는 셀이 렌더링될 때 셀로 적용된다.

그림 8-3. 셀 렌더러 프로퍼티 적용하기


그림 8-3에 G_TYPE_STRING과 GDK_TYPE_COLOR 타입으로 된 모델 열이 두 개 있다. 이는 GtkCellRendererText의 text와 foreground 프로퍼티로 적용되고, 그에 따라 트리 뷰 열을 렌더링하는 데 사용된다.


셀 데이터 함수를 정의하는 방법 또한 모든 셀 렌더러 프로퍼티를 변경하는 데 사용할 수 있다. 이 함수는 셀이 렌더링되기 전에 트리 뷰 내의 모든 행에 대해 호출될 것이다. 따라서 트리 모델에 데이터가 저장될 필요 없이 모든 셀이 어떻게 렌더링될 것인지 맞춤설정할 수 있다. 예를 들어, 셀 데이터 함수를 이용해 부동 소수점 수의 소수 자리수를 정의할 수 있다. 셀 데이터 함수는 이번 장의 "셀 데이터 함수"절에서 상세히 다룰 것이다.


그 다음으로는 텍스트(문자열, 숫자, Boolean 값), 토글 버튼, 스핀 버튼, 진행 막대, pixbuf, 콤보 박스, 키보드 가속기를 표시하는 데 사용되는 셀 렌더러를 다룰 것이다. 뿐만 아니라 개발자는 커스텀 렌더러 타입을 생성할 수도 있지만 GTK+는 현재 다양한 타입을 제공하고 있기 때문에 굳이 생성할 필요는 없을 것이다.


이번 절에서는 GtkTreeView 위젯을 사용하는 데 필요한 객체는 무엇인지, 그러한 객체들은 어떤 일을 하고 어떻게 상호연관되는지 학습하였다. 이제 GtkTreeView 위젯에 대해 기본적으로 이해할 것이니 다음 절에서는 GtkListStore 트리 모델을 이용하는 간단한 예제를 제공하겠다.


GtkListStore 이용하기

GtkTreeModel은 GtkListStore과 같은 데이터 저장소에 의해 구현되는 인터페이스에 불과하다는 점을 상기해보자. GtkListStore는 행들 간에 상속구조적 관계가 없는 데이터의 리스트를 생성하는 데 사용된다.


이번 절에서는 간단한 Grocery List 애플리케이션을 구현할 것인데, 애플리케이션에는 GtkCellRendererText를 이용하는 세 개의 열이 포함되어 있다. 이러한 애플리케이션의 스크린샷은 그림 8-4에서 확인할 수 있다. 첫 열은 TRUE 또는 FALSE를 표시하는 gboolean 값으로, 제품의 구매 여부를 정의한다.


Gtkd tip.png Boolean 열이 많으면 사용자가 관리하기 힘들어지기 때문에 Boolean 값을 텍스트로 표시하지 않는 편을 선호할 것이다. 이 대신 토글 버튼의 이용을 원할 것이다. GtkCellRendererToggle을 이용해 토글 버튼을 구현하는 방법은 후에 학습할 것이다. 부울 값은 종종 셀 렌더러 프로퍼티를 정의하기 위해 열의 속성으로 사용되기도 한다.


두 번째 열은 구매해야 할 제품의 수량을 정수로 표시하고, 세 번째 열은 제품을 설명하는 텍스트 문자열에 해당한다. 모든 열은 렌더링에 GtkCellRendererText를 이용하는데, GtkCellRendererText는 부울 값과 다양한 숫자 포맷(int, double, float)을 텍스트 문자열로 표시하는 기능이 있는 셀 렌더러다.

그림 8-4. GtkListStore 트리 모델을 이용하는 트리 뷰 위젯


리스팅 8-1은 식료품 리스트를 표시하는 GtkListStore 객체를 생성한다. 리스트 저장소는 제품을 표시할 뿐만 아니라 제품의 구매 여부와 구매 수량을 표시하기도 한다.


이러한 Grocery List 애플리케이션은 이번 장에서 여러 예제에 걸쳐 사용될 것이다. 따라서 앞의 예제에서 제시된 함수의 내용은 뒷부분에서 제외시킬지도 모른다. 또 일관된 구조화를 위해 모든 예제에서는 setup_tree_view()를 이용해 열과 렌더러를 설정하였다. 각 예제에 대한 코드 리스팅은 모두 www.gtkbook.com에서 다운로드가 가능하다.


리스팅 8-1. GtkTreeView 생성하기 (liststore.c)

#include <gtk/gtk.h>

enum
{
    BUY_IT = 0,
    QUANTITY,
    PRODUCT,
    COLUMNS
};

typedef struct
{
    gboolean buy;
    gint quantity;
    gchar *product;
} GroceryItem;

const GroceryItem list[] =
{
    { TRUE, 1, "Paper Towels" },
    { TRUE, 2, "Bread" },
    { FALSE, 1, "Butter" },
    { TRUE, 1, "Milk" },
    { FALSE, 3, "Chips" },
    { TRUE, 4, "Soda" },
    { FALSE, 0, NULL }
};

static void setup_tree_view (GtkWidget*);

int main (int argc,
        char *argv[])
{
    GtkWidget *window, *treeview, *scrolled_win;
    GtkListStore *store;
    GtkTreeIter iter;
    guint i = 0;

    gtk_init (&argc, &argv);

    window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
    gtk_window_set_title (GTK_WINDOW (window), "Grocery List");
    gtk_container_set_border_width (GTK_CONTAINER (window), 10);
    gtk_widget_set_size_request (window, 250, 175);

    treeview = gtk_tree_view_new ();
    setup_tree_view (treeview);

    /* Create a new tree model with three columns, as string, gint and guint. */
    store = gtk_list_store_new (COLUMNS, G_TYPE_BOOLEAN, G_TYPE_INT, G_TYPE_STRING);

    /* Add all of the products to the GtkListStore. */
    while (list[i].product != NULL)
    {
        gtk_list_store_append (store, &iter);
        gtk_list_store_set (store, &iter, BUY_IT, list[i].buy,
            QUANTITY, list[i].quantity, PRODUCT,
 list[i].product, -1);
        i++;
    }

    /* Add the tree model to the tree view and unreference it so that the model will
    * be destroyed along with the tree view. */
    gtk_tree_view_set_model (GTK_TREE_VIEW (treeview), GTK_TREE_MODEL (store));
    g_object_unref (store);

    scrolled_win = gtk_scrolled_window_new (NULL, NULL);
    gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_win),
        GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);

    gtk_container_add (GTK_CONTAINER (scrolled_win), treeview);
    gtk_container_add (GTK_CONTAINER (window), scrolled_win);
    gtk_widget_show_all (window);

    gtk_main ();
    return 0;
}

/* Add three columns to the GtkTreeView. All three of the columns will be
* displayed as text, although one is a gboolean value and another is
* an integer. */
static void
setup_tree_view (GtkWidget *treeview)
{
    GtkCellRenderer *renderer;
    GtkTreeViewColumn *column;

    /* Create a new GtkCellRendererText, add it to the tree view column and
    * append the column to the tree view. */
    renderer = gtk_cell_renderer_text_new ();
    column = gtk_tree_view_column_new_with_attributes
        ("Buy", renderer, "text", BUY_IT, NULL);
    gtk_tree_view_append_column (GTK_TREE_VIEW (treeview), column);

    renderer = gtk_cell_renderer_text_new ();
    column = gtk_tree_view_column_new_with_attributes
        ("Count", renderer, "text", QUANTITY, NULL);
    gtk_tree_view_append_column (GTK_TREE_VIEW (treeview), column);

    renderer = gtk_cell_renderer_text_new ();
    column = gtk_tree_view_column_new_with_attributes
        ("Product", renderer, "text", PRODUCT, NULL);
    gtk_tree_view_append_column (GTK_TREE_VIEW (treeview), column);
}


트리 뷰 생성하기

GtkTreeView 위젯을 생성하는 것은 이 과정에서 가장 쉬운 부분에 속한다. 그저 gtk_tree_view_new()를 호출하기만 하면 된다. 초기화 시 기본 트리 모델을 추가하고 싶다면 gtk_tree_view_new_with_model()을 이용할 수도 있지만 gtk_tree_view_set_model()을 이용하면 초기화 후에 트리 모델을 GtkTreeView로 쉽게 적용 가능하다. gtk_tree_view_new_with_model() 함수는 편의 함수에 불과하다.


자신의 요구에 맞도록 GtkTreeView를 맞춤설정할 수 있는 함수에는 다수가 있다. 가령 각 GtkTreeViewColumn 위에는 헤더 라벨이 렌더링되어 열 내용에 관한 추가 정보를 사용자에게 알려주기도 한다. 헤더 라벨을 숨기려면 gtk_tree_view_set_headers_visible()을 FALSE로 설정하면 된다.

void gtk_tree_view_set_headers_visible (GtkTreeView *treeview,
        gboolean visible);


Gtkd note.png 트리 뷰 헤더는 각 열의 내용을 사용자에게 알려주는 데 유용하기 때문에 헤더를 숨길 때는 주의해야 한다. 열의 개수가 1개 이하이거나 각 열의 내용이 어떤 방식으로든 명확히 설명되는 경우에만 숨겨야 한다.


GtkTreeViewColumn 헤더는 일부 트리 뷰에 대해 열 제목 이상의 기능을 제공한다. 정렬 가능한 트리 모델에서 열 헤더를 클릭하면 해당 열에 보유된 데이터에 따라 모든 열이 정렬되기 시작한다. 또 적용 가능하다면 열의 정렬 순서를 시각적으로 표시하기도 한다. 사용자가 트리 뷰 행의 정렬을 필요로 한다면 헤더를 숨겨선 안 된다.


또 다른 GtkTreeView 함수인 gtk_tree_view_set_rules_hint()는 GTK+ 테마(theme)에게 교호하는 행들(alternating rows)을 구별할 것을 요청한다. 이는 주로 근접하는 행의 배경색을 변경하여 이루어진다. 하지만 함수명에서 알 수 있듯이 해당 프로퍼티는 테마 엔진에 대한 힌트에 불과하여 중요하게 취급되지 않을 수도 있다. 또 일부 엔진은 이러한 설정과 상관없이 배경색을 자동으로 교대로 사용한다.

void gtk_tree_view_set_rules_hint (GtkTreeView *treeview,
        gboolean alternate_colors);


해당 프로퍼티는 꼭 필요할 때만 사용해야 한다. 예를 들어, 자신의 트리 뷰가 여러 개의 행을 포함한다면 사용자가 그 내용을 훑어보는 것이 도움이 될 것이다. 반대로 이러한 설정은 항상 사용자의 테마에 의해 결정되기 때문에 미적인 목적으로 사용되어선 안 된다.


GTK+ 개발자로서 당신은 시각적 프로퍼티를 변경 시 매우 주의를 기울여야 한다. 사용자는 그들의 요구에 맞는 테마를 선택할 수 있는 능력이 있으며, 개발자는 위젯이 표시되는 방법을 변경함으로써 자신의 애플리케이션을 색다르게 만들 수 있다.


렌더러와 열

GtkTreeView를 생성한 후에는 용도에 맞게 하나 또는 이상의 열을 뷰로 추가할 필요가 있다. 각 GtkTreeViewClumn은 내용을 간략하게 설명하는 헤더와 최소 하나의 셀 렌더러로 구성된다. 트리 뷰 열은 사실상 어떤 내용도 렌더링하지 않는다. 트리 뷰 열은 화면에 데이터를 그리는 데 사용되는 하나 또는 이상의 셀 렌더러를 보유한다.


모든 셀 렌더러는 GtkCellRenderer 클래스에서 파생되는데, GtkCellRenderer는 GtkWidget이 아니라 GtkObject에서 직접 파생되기 때문에 이번 장에서는 객체로 부른다. 각 셀 렌더러는 데이터가 셀 내에서 어떻게 그려지는지 결정하는 다수의 프로퍼티를 포함한다.


GtkCellRenderer 클래스는 모든 파생 렌더러(derivative renderer)에게 배경색, 크기 매개변수, 정렬(alignment), 시각성, 민감도, 패딩을 포함해 공통된 프로퍼티를 제공한다. GtkCellRenderer 프로퍼티의 전체 리스트는 부록 A에서 찾을 수 있다. 이는 editing-canceled 와 editing-started 시그널을 제공하여 커스텀 셀 렌더러에서 편집을 구현하도록 해준다.


리스팅 8-1에서는 문자열, 숫자, gboolean 값을 텍스트로 렌더링할 수 있는 GtkCellRendererText를 소개하였다. 텍스트로 된 셀의 렌더러는 gtk_cell_renderer_text_new()로 초기화된다.


GtkCellRendererText는 각 셀이 어떻게 렌더링될 것인지를 결정하는 추가 프로퍼티를 다수 제공한다. text 프로퍼티는 항상 설정해야 하는데, 이는 셀에 표시되는 문자열을 나타낸다. 나머지 프로퍼티는 텍스트 태그에 사용되는 것과 유사하다.


GtkCellRendererText는 각 행이 어떻게 렌더링될 것인지를 결정하는 수많은 프로퍼티를 포함한다. 아래 예제에서는 g_object_set()을 이용해 렌더러 내 모든 텍스트 부분의 전경색을 오렌지색으로 설정하였다. 일부 프로퍼티들은 그에 상응하는 set 프로퍼티를 갖기도 하는데, 값이 사용되길 원한다면 TRUE로 설정되어야 한다. 예를 들어, 변경내용이 효력을 발휘하기 위해선 foreground-set를 TRUE로 설정해야 한다.

g_object_set (G_OBJECT (renderer), "foreground", "Orange",
        "foreground-set", TRUE, NULL);


셀 렌더러를 생성했다면 GtkTreeViewColumn으로 추가해야 한다. 열에서 하나의 셀 렌더러만 표시하길 원한다면 gtk_tree_view_column_new_with_attributes()를 이용해 트리 뷰 열을 생성할 수 있다. 아래 코드에서는 "Buy"라는 제목으로 된 트리 뷰 열과 속성이 하나인 렌더러가 생성된다. GtkListStore가 채워질(populated) 때 이러한 속성을 BUY_IT이라 부를 것이다.

column = gtk_tree_view_column_new_with_attributes ("Buy", renderer,
        "text", BUY_IT, NULL);


위의 함수는 열 헤더에 표시할 문자열, 셀 렌더러, NULL로 끝나는 속성 리스트를 수락한다. 각 속성은 렌더러 프로퍼티를 참조하는 문자열과 트리 뷰의 열 번호를 포함한다. 여기서 gtk_tree_view_column_new_with_attributes()로 제공된 열 번호는 트리 모델 열을 참조하는데 트리 뷰가 사용하는 셀 렌더러나 트리 모델 열의 개수와 같지 않을 수도 있다는 사실을 기억하는 것이 중요하다.


아래 네 줄의 코드는 gtk_tree_view_column_new_with_attributes()가 제공하는 것과 동일한 코드를 구현한다. gtk_tree_view_colun_new()를 이용해 빈 열이 생성되고, 열의 제목은 "Buy"로 설정된다.

column = gtk_tree_view_column_new ();
gtk_tree_view_column_set_title (column, "Buy");
gtk_tree_view_column_pack_start (column, renderer, FALSE);
gtk_tree_view_column_set_attributes (column, renderer, "text", BUY_IT, NULL);


다음으로 셀 렌더러가 열로 추가된다. gtk_tree_view_column_pack_start()는 세 번째 Boolean 매개변수를 수락하는데, 이를 TRUE로 설정하면 열에게 수평으로 확장하여 추가 공간을 차지하도록 지시한다. 마지막 함수 gtk_tree_view_column_set_attributes()는 NULL로 끝나는 속성 리스트를 추가하여 개발자가 트리 뷰로 추가하는 행마다 맞춤설정될 것이다. 이러한 속성들은 명시된 렌더러에 적용된다.


gtk_tree_view_column_pack_start()를 호출하면 명시된 셀 렌더러와 이전에 연관된 속성을 모두 제거한다. 이를 피하려면 gtk_tree_view_column_add_attribute()를 이용해 한 번에 특정 셀 렌더러에 해당하는 열로 속성을 추가하는 수도 있다. 두 함수 모두 GtkTreeViewColumn이 하나 이상의 셀 렌더러를 포함할 때 유용하겠다.

void gtk_tree_view_column_add_attribute (GtkTreeViewColumn *column,
        GtkCellRenderer *renderer,
        const gchar *attribute,
        gint column);


다수의 렌더러를 트리 뷰 열로 추가하고 싶다면 각 렌더러를 패킹하고 속성을 따로 설정해야 할 것이다. 가령 파일 관리자에서는 동일한 열에 텍스트와 이미지 렌더러를 포함시키길 원할 것이다. 하지만 모든 열이 하나의 셀 렌더러만 필요로 한다면 gtk_tree_view_column_new_with_attributes()를 이용하는 편이 가장 수월할 것이다.


Gtkd note.png 전경색과 같은 프로퍼티를 열 내의 모든 행에 대해 동일한 값으로 설정하고 싶다면 g_object_set()를 이용해 각 프로퍼티를 셀 렌더러로 직접 적용해야 한다. 하지만 프로퍼티가 행에 따라 다를 것으로 추정된다면 주어진 렌더러에 해당하는 열의 속성으로 추가해야 한다.


트리 뷰 열의 준비가 완료되면 gtk_tree_view_append_column()을 이용해 트리 뷰로 추가되어야 한다. gtk_tree_view_insert_column()을 이용해 트리 뷰의 임의 위치로 열을 추가하거나 gtk_tree_view_remove_column()을 이용해 뷰를 제거하는 것도 가능하다.


GtkListStore 생성하기

이제 원하는 셀 렌더러와 함께 트리 뷰 열이 준비되었으니 렌더러와 트리 뷰를 접속하게 될 트리 모델을 생성할 때가 되었다. 리스팅 8-1에 실린 예에서는 GtkListStore를 이용해 항목을 요소의 리스트로 표시되도록 하였다.


새로운 리스트 저장소는 gtk_list_store_new()를 이용해 생성된다. 이 함수는 열의 개수와 각 열이 보유하게 될 데이터 유형을 수락한다. 리스팅 8-1에서 리스트 저장소는 gboolean, 정수, 문자열 데이터 유형을 저장하는 세 개의 열을 갖고 있다.

GtkListStore* gtk_list_store_new (gint n_columns,
        /* List of column types */);


리스트 저장소를 생성한 후에는 용도에 맞게 gtk_list_store_append()를 이용해 행을 추가할 필요가 있다. 이 함수는 리스트 저장소로 새 행을 추가하고, 반복자가 새로운 행을 가리키도록 설정될 것이다. 트리 반복자는 이번 장의 마지막 절에서 더 다루도록 하겠다. 지금은 새로운 트리 뷰 행을 가리키는지만 알아도 충분하다.

void gtk_list_store_append (GtkListStore *store,
        GtkTreeIter *iter);


gtk_list_store_prepend()와 gtk_list_store_insert()를 포함해 행을 리스트 보관소로 추가하기 위한 함수들도 여러 가지가 있다. 이용 가능한 함수의 전체 목록은 GtkListStore API 문서에서 찾을 수 있다.


행의 추가 외에도 gtk_list_store_remove()를 이용하면 행을 제거하는 것이 가능하다. 해당 함수는 GtkTreeIter가 참조하는 행을 제거할 것이다. 행이 제거되고 나면 반복자는 리스트 저장소에서 다음 행을 가리키고, 함수는 TRUE를 리턴할 것이다. 마지막 행이 방금 제거되었다면 반복자는 무효화될 것이며, 함수는 FALSE를 리턴할 것이다.

gboolean gtk_list_store_remove (GtkListStore *store,
        GtkTreeIter *iter);


그 외에도 리스트 저장소에서 모든 행을 제거하도록 gtk_list_store_clear()가 제공된다. 이 함수를 사용하면 데이터를 포함하지 않는 GtkListStore만 남는다. 그 시점이 지나서 객체가 사용되지 않으면 참조해제되어야 한다.


이제 행이 있으니 데이터를 추가해야 되는데, 그 때는 gtk_list_store_set()을 이용하면 된다. gtk_list_store_set() 함수는 열 번호 매개변수와 값 매개변수의 쌍으로 된 리스트를 수신한다. 예를 들면, BUY_IT으로 참조되는 아래 함수 호출에서 첫 번째 열은 제품의 구매 여부를 정의하는 Boolean 값을 수락한다. 이러한 값들은 gtk_list_store_new()에서 설정한 값에 해당한다.

gtk_list_store_set (store, &iter, BUY_IT, list[i].buy,
        QUANTITY, list[i].quantity, PRODUCT, list[i].product, -1);


gtk_list_store_set()의 마지막 요소는 -1로 설정되어야만 GTK+는 매개변수가 없음을 알 것이다. 그 외의 경우 terminal 출력에 끝없는 경고와 오류 리스트가 사용자에게 제시될 것이다.


Gtkd note.png GtkCellRendererText는 Boolean 값과 숫자를 화면에 렌더링이 가능한 텍스트 문자열로 자동 변환한다. 따라서 text 속성 열에 적용되는 데이터 유형은 텍스트 자체일 필요는 없지만 GtkListStore가 초기화되는 동안 정의된 리스트 저장소 열 타입과는 일치해야 한다.


리스트 저장소가 생성되고 나면 gtk_tree_view_set_model()을 호출하여 트리 뷰로 추가해야 한다. 이 함수를 호출하면 트리 모델의 참조 계수가 1씩 증가할 것이다. 따라서 트리 뷰가 소멸될 때 트리 뷰도 소멸되길 원한다면 리스트 저장소에서 g_object_unref()를 호출할 필요가 있다.


GtkTreeStore 이용하기

또 다른 내장된 트리 모델로 GtkTreeStore라고 불리는 타입이 있는데, 이는 다수준 트리 구조체로 행을 구조화한다. GtkTreeStore 트리 모델을 이용해 리스트를 구현하는 것도 가능하지만 객체가 만일 행에 하나 또는 그 이상의 자식이 있는 것으로 추측하면 오버헤드가 발생할 수 있기 때문에 권하지 않는다.


그림 8-5는 두 개의 루트 요소를 포함하는 트리 저장소의 예를 보여주는데, 각 요소에는 고유의 자식들이 있다. 자식이 있는 행의 좌측에 익스팬더(expander)를 클릭하면 자식을 표시하고 숨길 수 있다. 이는 GtkExpander 위젯이 제공하는 기능과 유사하다.

그림 8-5. GtkTreeStore 트리 모델을 이용한 트리 뷰 위젯


GtkListStore 대신 GtkTreeStore를 이용해 GtkTreeView를 구현 시 유일한 차이점은 저장소의 생성과 관련된다. 열은 모델이 아니라 뷰에 속하기 때문에 두 모델에서 모두 열과 렌더러가 동일한 방식으로 추가되므로 리스팅 8-2에서는 setup_tree_vew()의 구현은 제외시켰다.


리스팅 8-2는 원래의 Grocery List 애플리케이션을 수정하여 제품을 범주로 나누었다. 리스트는 두 가지 범주, 즉 Cleaning Supplies(청소용품)와 Food(식품)를 포함하고 두 범주 모두 고유의 자식을 갖는다. 각 범주의 수량은 런타임 시 계산되기 때문에 처음에 0으로 설정된다.


리스팅 8-2. GtkTreeStore 생성하기 (treestore.c)

#include <gtk/gtk.h>

enum
{
    BUY_IT = 0,
    QUANTITY,
    PRODUCT,
    COLUMNS
};

enum
{
    PRODUCT_CATEGORY,
    PRODUCT_CHILD
};

typedef struct
{
    gint product_type;
    gboolean buy;
    gint quantity;
    gchar *product;
} GroceryItem;

GroceryItem list[] =
{
    { PRODUCT_CATEGORY, TRUE, 0, "Cleaning Supplies" },
    { PRODUCT_CHILD, TRUE, 1, "Paper Towels" },
    { PRODUCT_CHILD, TRUE, 3, "Toilet Paper" },
    { PRODUCT_CATEGORY, TRUE, 0, "Food" },
    { PRODUCT_CHILD, TRUE, 2, "Bread" },
    { PRODUCT_CHILD, FALSE, 1, "Butter" },
    { PRODUCT_CHILD, TRUE, 1, "Milk" },
    { PRODUCT_CHILD, FALSE, 3, "Chips" },
    { PRODUCT_CHILD, TRUE, 4, "Soda" },
    { PRODUCT_CATEGORY, FALSE, 0, NULL }
};

/* The implementation of this function is the same as in Listing 8-1. */
static void setup_tree_view (GtkWidget*);

int main (int argc,
        char *argv[])
{
    GtkWidget *window, *treeview, *scrolled_win;
    GtkTreeStore *store;
    GtkTreeIter iter, child;
    guint i = 0, j;

    gtk_init (&argc, &argv);

    window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
    gtk_window_set_title (GTK_WINDOW (window), "Grocery List");
    gtk_container_set_border_width (GTK_CONTAINER (window), 10);
    gtk_widget_set_size_request (window, 275, 300);

    treeview = gtk_tree_view_new ();
    setup_tree_view (treeview);

    store = gtk_tree_store_new (COLUMNS, G_TYPE_BOOLEAN, G_TYPE_INT, G_TYPE_STRING);

    while (list[i].product != NULL)
    {
        /* If the product type is a category, count the quantity of all of the products
        * in the category that are going to be bought. */
        if (list[i].product_type == PRODUCT_CATEGORY)
        {
            j = i + 1;

            /* Calculate how many products will be bought in the category. */
            while (list[j].product != NULL && list[j].product_type != PRODUCT_CATEGORY)
            {
                if (list[j].buy)
                    list[i].quantity += list[j].quantity;
                j++;
            }

            /* Add the category as a new root element. */
            gtk_tree_store_append (store, &iter, NULL);
            gtk_tree_store_set (store, &iter, BUY_IT, list[i].buy,
                QUANTITY, list[i].quantity, PRODUCT, list[i].product, -1);
        }

        /* Otherwise, add the product as a child of the category. */
        else
        {
            gtk_tree_store_append (store, &child, &iter);
            gtk_tree_store_set (store, &child, BUY_IT, list[i].buy,
                QUANTITY, list[i].quantity, PRODUCT, list[i].product, -1);
        }

        i++;
    }

    gtk_tree_view_set_model (GTK_TREE_VIEW (treeview), GTK_TREE_MODEL (store));
    gtk_tree_view_expand_all (GTK_TREE_VIEW (treeview));
    g_object_unref (store);

    scrolled_win = gtk_scrolled_window_new (NULL, NULL);
    gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_win),
        GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);

    gtk_container_add (GTK_CONTAINER (scrolled_win), treeview);
    gtk_container_add (GTK_CONTAINER (window), scrolled_win);
    gtk_widget_show_all (window);

    gtk_main ();
    return 0;
}


트리 저장소는 gtk_list_store_new()와 동일한 매개변수를 수락하는 gtk_tree_store_new()를 이용해 초기화된다. 매개변수로는 데이터 열의 개수, 그리고 각 트리 모델 열에 상응하는 데이터 유형의 리스트가 있다.


트리 저장소에 행을 추가하는 일은 리스트 저장소에 행을 추가하는 것과는 약간 다르다. 트리 저장소에 행을 추가하려면 gtk_tree_store_append()를 이용해야 하는데, 이 함수는 하나가 아니라 두 개의 반복자를 수락한다. 첫 번째 반복자는 함수가 리턴될 때 삽입되는 행을 가리키고, 두 번째 반복자는 새로운 행의 부모 행을 가리켜야 한다.

gtk_tree_store_append (store, &iter, NULL);


앞에서 gtk_tree_store_append()를 호출할 때 NULL을 부모 반복자로 전달함으로써 루트 요소가 리스트 뒤에 추가되었다. 그리고 iter 트리 반복자는 새로운 행의 위치로 설정되었다. 첫 번째 반복자의 현재 내용은 함수가 리턴될 때 겹쳐서 작성될 것이기 때문에 이미 초기화되어 있을 필요는 없다.


gtk_tree_store_append()를 두 번째 호출하면 행이 iter의 자식으로 추가될 것이다. 다음으로 함수가 리턴될 때 child 트리 반복자는 트리 저장소 내에서 새로운 행의 현재 위치로 설정될 것이다.

gtk_tree_store_append (store, &child, &iter);


리스트 저장소와 마찬가지로 트리 저장소에도 행을 추가 시 이용 가능한 함수들이 많이 있다. 가령 gtk_tree_store_insert(), gtk_tree_store_prepend(), gtk_tree_store_insert_before를 몇 가지로 예로 들 수 있겠다. 함수의 전체 목록은 GtkTreeStore API 문서를 참고하도록 한다.


행을 트리 저장소로 추가하고 나면 그것은 데이터가 없는 빈 행에 불과하다. 행에 데이터를 추가하기 위해서는 gtk_tree_store_set()를 호출한다. 이 함수는 gtk_list_store_set()과 같은 방식으로 작동한다. 트리 저장소, 행의 위치를 가리키는 트리 반복자, -1로 끝이 나는 행-데이터 쌍의 리스트를 수락한다. 이러한 행 번호는 셀 런더러 속성을 준비할 때 사용한 번호에 해당한다.

gtk_tree_store_set (store, &child, BUY_IT, list[i].buy, QUANTITY, list[i].quantity,
        PRODUCT, list[i].product, -1);


트리 저장소에 행을 추가하는 것 외에도 gtk_tree_store_remove()를 이용하면 행을 제거할 수 있다. 해당 함수는 GtkTreeIter가 참조하는 행을 제거할 것이다. 행이 제거되고 나면 iter는 트리 저장소에서 다음 행을 가리킬 것이고, 함수는 TRUE를 리턴할 것이다. 자신이 제거한 행이 트리 저장소의 마지막 행이라면 반복자는 무효해지고 함수는 FALSE를 리턴한다.

gboolean gtk_tree_store_remove (GtkTreeStore *store,
        GtkTreeIter *iter);


게다가 트리 저장소에서 모든 행을 제거하는 데 이용할 수 있도록 gtk_tree_store_clear()가 제공된다. 그리고 나면 어떤 데이터도 포함하지 않은 GtkTreeStore만 남게 된다. 그 시점 이후로 객체를 이용할 일이 없다면 참조해제 되어야 한다.


리스팅 8-2에서는 gtk_main()을 호출하기 전에 gtk_tree_view_expand_all()이 호출되어 모든 행을 확장시켰다. 이는 자식-부모 행 관계를 가진 트리 모델에만 영향을 미치겠지만 가능한 모든 행을 확장시키는 재귀 함수다. 그 외에도 gtk_tree_view_collapse_all()을 이용하면 모든 행을 붕괴(collapse)시킬 수 있다. 모든 행은 기본적으로 붕괴될 것이다.


행 참조하기

트리 모델 내에서 특정 행을 참조할 때 사용 가능한 객체로 세 가지가 있는데, 각각은 고유의 장점을 지닌다. 세 가지 객체는 바로 GtkTreePath, GtkTreeIter, GtkTreeRowReference이다. 지금부터 여러 절에 걸쳐 이러한 객체가 각각 어떻게 작동하는지, 자신의 프로그램에서 어떻게 사용할 것인지 학습하게 될 것이다.

트리 경로

GtkTreePath는 트리 모델의 행을 참조하기에 매우 편리한 객체인데, 그 이유는 사람이 읽을 수 있는 문자열로 쉽게 표현이 가능하기 때문이다. 그 뿐만 아니라 부호가 없는 정수의 배열로 표현되기도 한다.


가령 문자열 3:7:5가 표시되면 당신은 네 번째 루트 요소에서 시작할 것이다 (색인은 0부터 시작되므로 요소 3은 그 수준에서 사실상 네 번째 요소가 된다는 점을 상기해보자). 다음으로 루트 요소의 여덟 번째 자식으로 진행할 것이다. 문제가 되는 행은 자식의 여섯 번째 자식이다.


이를 그래픽하게 설명하기 위해 그림 8-6에는 리스팅 8-2에서 생성한 트리 뷰와 함께 트리 경로에 라벨을 표시하였다. 각 루트 요소는 0과 1, 하나의 요소로만 참조되었다. 첫 번째 루트 요소에는 두 개의 자식이 있고 각각 0:0과 0:1로 참조되었다.

그림 8-6. GtkTreeStore를 이용한 트리 뷰의 세 가지 경로


경로와 그에 해당하는 문자열 간 서로 변환이 가능하도록 두 가지 함수, gtk_tree_path_to_string()과 gtk_tree_path_new_from_string()이 제공된다. 트리 뷰의 상태를 저장하려는 것이 아닌 이상 문자열 경로를 직접 처리할 일은 별로 없지만, 직접 처리를 한다면 트리 경로가 어떻게 작동하는지 이해하는 데 도움이 된다.


리스팅 8-3은 트리 경로의 이용을 보여주는 간단한 예를 제시한다. 가장 먼저 Bread 제품 행을 가리키는 새로운 경로를 생성함으로써 시작된다. 다음으로 gtk_tree_path_up()을 이용해 경로에서 한 수준을 이동한다. 경로를 다시 문자열로 변환할 때는 결과 출력이 1로, Food 행을 가리킴을 볼 수 있다.


리스팅 8-3. 경로와 문자열 간 변환

GtkTreePath *path;
gchar *str;

path = gtk_tree_path_new_from_string ("1:0"); /* Point to bread */
gtk_tree_path_up (path);
str = gtk_tree_path_to_string (path);
g_print (str);
g_free (str);


Gtkd tip.png 트리 반복자를 얻어서 경로 문자열만 이용 가능하도록 만들고 싶다면 문자열을 GtkTreePath로 변환한 후 GtkTreeIter로 변환할 수도 있다. 하지만 중간 단계를 생략하고 트리 경로 문자열을 곧바로 트리 반복자로 변환하는 gtk_tree_model_get_iter_from_string()을 이용하는 방법이 더 낫다.


gtk_tree_path_up() 외에 트리 모델을 훑어보도록 도와주는 함수들이 있다. gtk_tree_path_down()을 이용하면 자식 행으로 이동이 가능하고, gtk_tree_path_next()와 gtk_tree_path_prev()를 이용하면 동일한 수준에서 다음 행이나 이전 행으로 이동이 가능하다. 이전 행이나 부모 행으로 이동이 실패하면 FALSE가 리턴될 것이다.


때로는 트리 경로를 문자열 대신 정수의 리스트로 갖길 원할 때가 있다. 그때 gtk_tree_path_get_indices() 함수를 이용하면 경로 문자열을 구성하는 정수를 리턴할 것이다.

gint* gtk_tree_path_get_indices (GtkTreePath *path);


단, 트리 모델에 행을 추가하거나 그로부터 제거할 때 트리 경로에 문제가 발생할 수 있다. 경로가 트리 내에 다른 행을 가리키거나 더 나쁜 경우, 행이 더 이상 존재하지 않는 결과가 발생하기도 한다! 가령, 트리 경로가 트리의 마지막 요소를 가리키는데 개발자가 그 행을 제거할 경우, 트리 경로는 트리의 한계를 넘어 가리킬 것이다. 이 문제를 해결하려면 트리 경로를 트리 행 참조로 변환하면 된다.


트리 행 참조

GtkTreeRowReference 객체는 트리 모델의 변경내용을 살펴보는 데 사용된다. 내부적으로는 row-inserted, row-deleted, rows-reordered 시그널로 연결되고, 변경내용을 기반으로 저장된 경로를 업데이트한다.


새로운 트리 행 참조는 기존에 존재하는 GtkTreeModel과 GtkTreePath로부터 gtk_tree_row_reference_new()를 이용해 이루어진다. 행 참조로 복사된 트리 경로는 모델 내에서 내용이 변경될 때 업데이트될 것이다.

GtkTreeRowReference* gtk_tree_row_reference_new (GtkTreeModel *model,
        GtkTreePath *path);


경로를 검색해야 한다면 gtk_tree_row_reference_get_path()를 이용할 수 있는데, 행이 모델에 더 이상 존재하지 않으면 NULL을 리턴할 것이다. 트리 행 참조는 트리 모델에 일어나는 변경내용을 바탕으로 트리 경로를 업데이트할 수 있지만, 트리 경로의 행과 동일한 수준에서 모든 요소를 제거할 경우 더 이상 가리킬 행이 없을 것이다.


리턴된 트리 경로는 사용이 끝나면 gtk_tree_path_free()를 이용해 해제되어야 한다. 트리 행 참조는 gtk_tree_row_reference_free()를 이용해 해제가 가능하다.


트리 행 참조는 트리 모델 내에 행을 추가, 제거, 또는 정렬 시 약간의 오버헤드 처리를 부가하게 됨을 인식해야 하는데, 이러한 액션에 의해 발생하는 모든 시그널은 참조들이 처리해야 하기 때문이다. 이러한 오버헤드는 대부분의 애플리케이션에선 문제가 되지 않는데, 사용자가 눈치챌 만큼 행의 양이 많지 않기 때문이다. 하지만 자신의 애플리케이션에 행의 수가 많이 포함되어 있다면 트리 행 참조를 현명하게 이용해야겠다.


트리 반복자

GTK+는 GtkTreeModel 내에서 특정 행을 참조하는 데 사용할 수 있도록 GtkTreeIter 객체를 제공한다. 이러한 반복자들은 모델에 의해 내부적으로 사용되므로, 개발자는 트리 반복자의 내용을 절대 직접적으로 수정해선 안 된다는 뜻이다.


GtkTreeIter의 예는 이미 여러 개를 살펴보았는데, 이를 통해 아마 트리 반복자가 GtkTextIter와 비슷한 방식으로 사용됨을 눈치챘을 것이다. 트리 반복자는 트리 모델의 조작에 사용된다. 하지만 트리 경로는 사람이 읽을 수 있는 인터페이스를 제공하는 방식으로 트리 모델 내의 행을 가리키는 데 사용된다. 트리 행 참조를 이용하면 트리 모델의 내용이 변경되더라도 트리 경로가 그것이 가리키는 장소를 조정하도록 확보할 수 있다.


GTK+는 트리 반복자에 연산을 실행하도록 다수의 내장된 함수를 제공한다. 일반적으로 모델에 행을 추가하고, 행의 내용을 설정하고, 모델의 내용을 검색하는 데에 반복자가 사용된다. 리스팅 8-1과 8-2에서는 트리 반복자를 이용해 GtkListStore와 GtkTreeStore 모델에 행이 추가된 후 각 행의 초기 내용이 설정되었다.


GtkTreeModel은 여러 개의 gtk_tree_model_iter_*() 함수를 제공하는데, 이들은 반복자를 이동시키고 그에 관한 정보를 검색하는 데 사용이 가능하다. 예를 들어, 다음 반복자 위치로 이동시키려면 gtk_tree_model_iter_next()를 이용할 수 있으며, 액션이 성공하면 TRUE가 리턴될 것이다. 이용 가능한 함수의 전체 목록은 GtkTreeModel API 문서에서 찾을 수 있다.


gtk_tree_model_get_path()와 gtk_tree_model_get_iter()를 이용하면 트리 반복자와 트리 경로 간에 쉽게 변환이 가능하다. 이러한 함수가 올바로 작동하기 위해서는 트리 경로 또는 트리 반복자가 유효해야 한다. 리스팅 8-4는 GtktreeIter와 GtkTreePath 간 변환 방법을 보여주는 간단한 예를 제시한다.


리스팅 8-4. 경로와 반복자 간 변환하기

path = gtk_tree_model_get_path (model, &iter);
gtk_tree_model_get_iter (model, &iter, path);
gtk_tree_path_free (path);


리스팅 8-4에서 첫 번째 함수 gtk_tree_model_get_path()는 유효한 트리 반복자를 트리 경로로 변환한다. 이후 경로는 gtk_tree_model_get_iter()로 전송되고, 이 함수는 다시 반복자로 변환한다. 트리 반복자는 포인터로서 취급되어야 하기 때문에 두 번째 함수는 세 개의 매개변수를 수락함을 주목한다.


GtkTreeIter가 제시하는 한 가지 문제는 모델이 편집된 후에 반복자가 꼭 존재할 것이라고 장담할 수 없다는 데에 있다. 물론 모든 경우에 해당하는 것은 아니며, gtk_tree_model_get_flags()를 이용하면 GTK_TREE_MODEL_ITERS_PERSIST 플래그를 확인할 수 있는데 이는 GtkListStore와 GtkTreeStore에 대해 기본적으로 켜져 있다. 해당 플래그가 설정되면 트리 반복자는 행이 존재하는 한 항상 유효할 것이다.

GtkTreeModelFlags gtk_tree_model_get_flags (GtkTreeModel *model);


반복자가 계속 존재하도록(persist) 설정되더라도 트리 반복자 객체를 저장하는 것은 좋은 생각이 아닌데, 이러한 객체는 트리 모델에 의해 내부적으로 사용되기 때문이다. 그 대신, 모델이 변경되면 참조는 무효해질 것이므로 시간의 경과에 따라 행을 추가하는 데에 트리 행 참조를 이용해야 한다.


행 추가하기와 선택 처리하기

지금까지 제시한 두 가지 예제 모두 시작 시 트리 모델을 정의한다. 따라서 내용은 처음에 설정된 후 변경되지 않았다. 이번 절에서는 Grocery List 애플리케이션을 확장시켜 사용자가 제품을 추가 및 제거할 수 있도록 만들 것이다. 예제를 소개하기 전에 단일 선택과 다중 선택을 처리하는 방법을 학습할 것이다.

단일 선택

각 트리 뷰에 대한 선택 정보는 GtkTreeSelection 객체가 보유한다. 해당 객체는 gtk_tree_view_get_selection()을 이용해 검색이 가능하다. GtkTreeSelection 객체는 GtkTreeView마다 자동으로 생성되기 때문에 자신만의 트리 선택을 생성할 필요가 전혀 없다.


Gtkd caution.png GtkTreeSelection은 선택내용이 변경되면 발생하는 하나의 시그널 changed를 제공한다. 하지만 이 시그널이 항상 신뢰도가 있는 것은 아니기 때문에 사용 시 주의해야 한다. 이는 사용자가 이미 선택된 행을 선택하여 어떤 내용도 변경되지 않을 때 발생한다. 따라서 선택 처리에 GtkTreeView가 제공하는 시그널을 이용하는 것이 최선의 방법으로, 이는 부록 B에서 찾을 수 있다.


트리 뷰는 여러 타입의 선택을 지원한다. gtk_tree_selection_set_mode()를 이용하면 선택 타입을 변경할 수 있다. 선택 타입은 아래의 값을 포함하는 GtkSelectionMode 열거에 의해 정의된다.

  • GTK_SELECTION_NONE: 사용자가 어떠한 행도 선택할 수 없다.
  • GTK_SELECTION_SINGLE: 사용자가 하나의 행까지 선택 가능하지만 행을 전혀 선택하지 않는 것도 가능하다. 기본적으로 트리 선택은 GTK_SELECTION_SINGLE을 이용해 초기화된다.
  • GTK_SELECTION_BROWSE: 사용자는 정확히 하나의 행을 선택할 수 있을 것이다. 드문 경우지만 선택된 행이 없을 수도 있다. 이 옵션은 선택내용이 다른 행으로 이동될 때를 제외하곤 사용자가 행의 선택을 해제하지 못하도록 한다.
  • GTK_SELECTION_MULTIPLE: 사용자가 원하는 수만큼 행을 선택할 수 있다. 사용자는 Ctrl 이나 Shift 키를 이용해 추가 요소 또는 요소의 범위를 선택할 수 있을 것이다.


GTK_SELECTION_SINGLE 또는 GTK_SELECTION_BROWSE와 같은 선택 타입을 정의했다면 하나의 행만 선택되도록 확보해야 한다. 하나의 선택만 있는 트리 뷰의 경우 gtk_tree_selection_get_selected()를 이용해 선택된 행을 검색할 수 있다.

gboolean gtk_tree_selection_get_selected (GtkTreeSelection *selection,
        GtkTreeModel **model,
        GtkTreeIter *iter);


gtk_tree_selection_get_selected() 함수를 이용하면 GtkTreeSelection 객체와 연관된 트리 모델과 선택된 행을 가리키는 트리 반복자를 검색할 수 있다. 모델과 반복자가 성공적으로 설정되면 TRUE가 리턴된다. 이 함수는 GTK_SELECTION_MULTIPLE의 선택 모드와는 작동하지 않을 것이다!


어떤 행도 선택되지 않았다면 트리 반복자가 NULL로 설정되고, 함수로부터 FALSE가 리턴될 것이다. 따라서 gtk_tree_selection_get_selected()를 선택된 행이 있는지 없는지 확인하는 테스트용으로 이용할 수 있겠다.


다중 선택

트리 선택이 다중 행의 선택을 허용할 경우 (GTK_SELECTION_MULTIPLE) 선택을 처리하는 방법이 두 가지가 있는데, 하나는 모든 행에 함수를 호출하는 것이고, 나머지는 선택된 모든 행을 GList로 검색하는 것이다. 첫 번째는 gtk_tree_selection_selected_foreach()를 이용해 선택된 행마다 함수를 호출한다.

gtk_tree_selection_selected_foreach (selection, foreach_func, NULL);


이 함수는 선택된 행마다 foreach_func()를 호출하도록 허용하여 선택적 gpointer 데이터 매개변수를 전달한다. 앞의 예제에서는 NULL이 함수로 전달되었다. 함수는 GtkTreeSelectionForeachFunc 타입이어야 하며, 그 예제는 리스팅 8-5에서 확인할 수 있다. 잇따른 GtkTreeSelectionForeachFunc는 제품 문자열을 검색하여 화면으로 출력한다.


리스팅 8-5. 선택된 For-Each 함수

static gboolean
foreach_func (GtkTreeModel *model,
        GtkTreePath *path,
        GtkTreeIter *iter,
        gpointer data)
{
    gchar *text;

    gtk_tree_model_get (model, iter, PRODUCT, &text, -1);
    g_print ("Selected Product: %s\n", text);
    g_free (text);
}


Gtkd note.png GtkTreeSelectionForeachFunc 구현 내부로부터 선택이나 트리 모델을 수정해선 안 된다! 이를 어길 시 무효한 트리 경로와 반복자가 야기되어 GTK+는 사용자에게 심각한 오류를 제시할 것이다.


foreach 함수에 트리 선택을 이용 시 한 가지 문제는 함수 내부로부터 선택을 조작할 수 없다는 데에 있다. 이러한 문제를 해결하기 위해서는 gtk_tree_selection_get_selected_rows()를 이용하는 편이 더 나은데, 이를 이용 시 각각이 선택된 행을 가리키는 GtkTreePath 객체들로 구성된 GList를 리턴한다.

GList* gtk_tree_selection_get_selected_rows (GtkTreeSelection *selection,
        GtkTreeModel **model);


이제 리스트 내의 각 행에 어느 정도의 연산을 실행할 수 있다. 하지만 주의해야 할 점이 있다. GList foreach 함수 내에서 트리 모델을 편집해야 한다면 먼저 모든 트리 경로를 트리 행 참조로 변환해야 할 것인데, 그래야만 개발자의 액션이 진행되는 동안 유효하게 유지되기 때문이다.


모든 행을 수동으로 순환(loop through)하길 원한다면 gtk_tree_selection_count_selected_rows()를 이용하는 방법도 있으며, 이는 현재 선택된 행의 개수를 리턴할 것이다. 리스트의 사용이 끝나면 리스트를 반복한 후(iterate through), 리스트를 해제하기 전에 트리 경로를 먼저 해제해야 한다.


새로운 행 추가하기

선택(selection)에 관해 소개하였으니 리스트로 새 제품을 추가하는 기능을 추가할 때가 되었다. 애플리케이션 대부분은 리스팅 8-2의 내용과 중복되므로 앞으로 소개할 세 개의 리스팅에서 제외시켰다.


이전 Grocery List 애플리케이션과 비교 시 이번 예제의 main() 함수에 나타나는 유일한 차이점을 그림 8-7에 실었으며, 트리 뷰의 하단을 따라 추가된 GTK_STOCK_ADD와 GTK_STOCK_REMOVE 버튼을 확인할 수 있다. 또 사용자가 한 번에 여러 개의 행을 선택할 수 있도록 선택 모드도 변경되었다.

그림 8-7. 식료품 리스트의 항목 편집하기


리스팅 8-6은 사용자가 Add 버튼을 클릭하면 실행되는 콜백 함수의 구현이다. 이는 사용자에게 GtkDialog를 제시하여 범주를 선택하고 제품명과 제품의 구매 수량을 입력하며 제품의 구매 여부를 선택할 것을 요청한다.


모든 필드가 유효해지면 선택된 범주 아래에 행이 추가된다. 그리고 사용자가 제품을 구매해야 한다고 명시하면 범주의 총 수량에 제품의 구매 수량이 더해진다.


리스팅 8-6. New Product 추가하기 (selections.c)

static void
add_product (GtkButton *add,
        GtkTreeView *treeview)
{
    GtkWidget *dialog, *table, *combobox, *entry, *spin, *check;
    GtkTreeIter iter, child;
    GtkTreePath *path;
    GtkTreeModel *model;
    const gchar *product;
    gchar *category, *name;
    gint quantity, i = 0;
    gboolean buy;

    /* Create a dialog that will be used to create a new product. */
    dialog = gtk_dialog_new_with_buttons ("Add a Product", NULL,
                GTK_DIALOG_MODAL,
                GTK_STOCK_ADD, GTK_RESPONSE_OK,
                GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
                NULL);

    /* Create widgets that will be packed into the dialog. */
    combobox = gtk_combo_box_new_text ();
    entry = gtk_entry_new ();
    spin = gtk_spin_button_new_with_range (0, 100, 1);
    check = gtk_check_button_new_with_mnemonic ("_Buy the Product");
    gtk_spin_button_set_digits (GTK_SPIN_BUTTON (spin), 0);

    /* Add all of the categories to the combo box. */
    while (list[i].product != NULL)
    {
        if (list[i].product_type == PRODUCT_CATEGORY)
            gtk_combo_box_append_text (GTK_COMBO_BOX (combobox), list[i].product);
        i++;
    }

    table = gtk_table_new (4, 2, FALSE);
    gtk_table_set_row_spacings (GTK_TABLE (table), 5);
    gtk_table_set_col_spacings (GTK_TABLE (table), 5);
    gtk_container_set_border_width (GTK_CONTAINER (table), 5);

    /* Pack the table that will hold the dialog widgets. */
    gtk_table_attach (GTK_TABLE (table), gtk_label_new ("Category:"), 0, 1, 0, 1,
                GTK_SHRINK | GTK_FILL, GTK_SHRINK | GTK_FILL, 0, 0);
    gtk_table_attach (GTK_TABLE (table), combobox, 1, 2, 0, 1, GTK_EXPAND | GTK_FILL,
                GTK_SHRINK | GTK_FILL, 0, 0);
    gtk_table_attach (GTK_TABLE (table), gtk_label_new ("Product:"), 0, 1, 1, 2,
                GTK_SHRINK | GTK_FILL, GTK_SHRINK | GTK_FILL, 0, 0);
    gtk_table_attach (GTK_TABLE (table), entry, 1, 2, 1, 2, GTK_EXPAND | GTK_FILL,
                GTK_SHRINK | GTK_FILL, 0, 0);
    gtk_table_attach (GTK_TABLE (table), gtk_label_new ("Quantity:"),
 0, 1, 2, 3,
                GTK_SHRINK | GTK_FILL, GTK_SHRINK | GTK_FILL, 0, 0);
    gtk_table_attach (GTK_TABLE (table), spin, 1, 2, 2, 3, GTK_EXPAND | GTK_FILL,
                GTK_SHRINK | GTK_FILL, 0, 0);
    gtk_table_attach (GTK_TABLE (table), check, 1, 2, 3, 4, GTK_EXPAND | GTK_FILL,
                GTK_SHRINK | GTK_FILL, 0, 0);

    gtk_box_pack_start_defaults (GTK_BOX (GTK_DIALOG (dialog)->vbox), table);
    gtk_widget_show_all (dialog);

    /* If the user presses OK, verify the entries and add the product. */
    if (gtk_dialog_run (GTK_DIALOG (dialog)) == GTK_RESPONSE_OK)
    {
        quantity = (gint) gtk_spin_button_get_value (GTK_SPIN_BUTTON (spin));
        product = gtk_entry_get_text (GTK_ENTRY (entry));
        category = gtk_combo_box_get_active_text (GTK_COMBO_BOX (combobox));
        buy = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (check));

        if (g_ascii_strcasecmp (product, "") || category == NULL)
        {
            g_warning ("All of the fields were not correctly filled out!");
            gtk_widget_destroy (dialog);

            if (category != NULL)
                g_free (category)
            return;
        }

        model = gtk_tree_view_get_model (treeview);
        gtk_tree_model_get_iter_from_string (model, &iter, "0");

        /* Retrieve an iterator pointing to the selected category. */
        do
        {
            gtk_tree_model_get (model, &iter, PRODUCT, &name, -1);

            if (g_ascii_strcasecmp (name, category) == 0)
            {
                g_free (name);
                break;
            }

            g_free (name);
        } while (gtk_tree_model_iter_next (model, &iter));

        /* Convert the category iterator to a path so that it will not become invalid
        * and add the new product as a child of the category. */
        path = gtk_tree_model_get_path (model, &iter);
        gtk_tree_store_append (GTK_TREE_STORE (model), &child, &iter);
        gtk_tree_store_set (GTK_TREE_STORE (model), &child, BUY_IT, buy,
                QUANTITY, quantity, PRODUCT, product, -1);

        /* Add the quantity to the running total if it is to be purchased. */
        if (buy)
        {
            gtk_tree_model_get_iter (model, &iter, path);
            gtk_tree_model_get (model, &iter, QUANTITY, &i, -1);
            i += quantity;
            gtk_tree_store_set (GTK_TREE_STORE (model), &iter, QUANTITY, i, -1);
        }

        gtk_tree_path_free (path);
        g_free (category);
    }

    gtk_widget_destroy (dialog);
}


행 데이터 검색하기

트리 모델 행에 저장된 값을 검색하는 작업은 행을 추가하는 작업과 매우 유사하다. 리스팅 8-6에서는 트리 뷰의 첫 번째 행을 가리키는 트리 반복자를 검색하기 위해 gtk_tree_model_get_iter_from_string()이 먼저 사용되었다. 이는 첫 번째 범주에 해당한다.


다음으로 gtk_tree_model_iter_next()를 이용해 모든 루트 수준의 행을 순환하였다. 각 루트 수준의 행마다 아래의 코드가 실행된다. 먼저 gtk_tree_model_get()을 이용해 제품명이 검색된다. 이 함수는 행을 가리키는 반복자인 GtkTreeModel을 비롯해 데이터를 저장하는 열 번호-변수 쌍의 리스트를 수락하는 gtk_tree_store_set()와 마찬가지로 작동한다. 해당 리스트는 -1로 끝나며, 프로그래머가 해제한 리턴값이어야 한다.

gtk_tree_model_get (model, &iter, PRODUCT, &name, -1);
if (g_ascii_strcasecmp (name, category) == 0)
    break;


이후 g_ascii_strcasecmp()가 사용되어 현재 제품을 선택된 범주명과 비교한다. 두 문자열이 매치하면 올바른 범주를 찾았기 때문에 루프가 종료된다. iter 변수는 이제 선택된 범주를 가리킨다.


새로운 행 추가하기

트리 모델에 새로운 행을 추가하는 작업은 시작 시 추가되는 것과 동일한 방식으로 이루어진다. 아래 코드에서 선택된 범주를 가리키는 GtkTreeIter는 먼저 트리 경로로 변환되는데, 이는 트리 저장소가 변경되면 무효화되기 때문이다. GtkTreeIter의 위치는 변경될 가능성이 거의 없기 때문에 트리 행 참조로 변경될 필요는 없다는 사실을 명심한다.

path = gtk_tree_model_get_path (model, &iter);
gtk_tree_store_append (GTK_TREE_STORE (model), &child, &iter);
gtk_tree_store_set (GTK_TREE_STORE (model), &child, BUY_IT, buy,
        QUANTITY, quantity, PRODUCT, product, -1);


다음으로 gtk_tree_store_append()를 이용해 새로운 행이 뒤에 추가되는데, 여기서 iter는 부모 행을 의미한다. 행은 대화상자에서 사용자가 입력된 데이터를 이용해 gtk_tree_store_set()으로 채워진다.


콤보 박스

리스팅 8-6은 GtkComboBox라고 불리는 새 위젯을 소개한다. GtkComboBox는 사용자가 드롭다운 리스트에 있는 여러 개의 옵션에서 선택하도록 해주는 위젯이다. 콤보 박스는 선택된 내용을 정상 상태로 표시한다.


콤보 상자는 위젯을 인스턴스하는 데 사용한 함수에 따라 두 가지 방법으로 이용이 가능한데, 커스텀 GtkTreeModel을 이용하는 방법과 문자열 열이 하나인 기본 모델을 이용하는 방법이 되겠다.


리스팅 8-6에서는 gtk_combo_box_new_text()를 이용해 새로운 GtkComboBox가 생성되었고, 이는 하나의 문자열 열을 포함하는 특수화된 콤보 박스를 생성한다. 이는 편의 함수에 불과한데, 콤보 박스의 드롭다운 리스트는 GtkTreeModel을 이용해 내부적으로 처리되기 때문이다. gtk_combo_box_new_text()를 이용해 생성된 콤보 박스에는 문자열만 수락 가능한 GtkTreeModel이 자동으로 생성된다. 따라서 아래 함수를 이용해 옵션을 쉽게 앞·뒤로 추가하고 새로운 옵션을 삽입하도록 해준다.

void gtk_combo_box_append_text (GtkComboBox *combobox,
        const gchar *text);
void gtk_combo_box_prepend_text (GtkComboBox *combobox,
        const gchar *text);
void gtk_combo_box_insert_text (GtkComboBox *combobox,
        gint position,
        const gchar *text);


뿐만 아니라 gtk_combo_box_remove_text()를 이용해 선택내용을 제거하고, gtk_combo_box_get_active_text()를 이용해 현재 선택된 문자열의 복사본을 검색하는 것도 가능하다. 하지만 이러한 함수들은 gtk_combo_box_new_text()의 도움으로 GtkComboBox를 초기화할 때에만 사용할 수 있다.


대부분의 콤보 박스는 gtk_combo_box_new()를 이용해 생성되는데, 이 함수는 개발자가 선택을 보유하는 트리 모델을 생성하여 gtk_combo_box_set_model()을 이용해 추가할 것을 요한다. 이는 각 열의 타입이나 트리 모델의 내용에 대해서는 어떠한 추측도 하지 않는다. 뿐만 아니라 다중 열을 가진 트리 모델도 지원된다.


gtk_combo_box_new()로 생성한 콤보 박스를 이용할 경우, 선택내용을 추가하거나 제거하는 작업은 전적으로 트리 모델이 처리하기 때문에 별도의 함수를 제공할 필요가 없다. 하지만 현재 선택을 검색하는 데에 사용할 수 있는 함수로 두 가지가 제공된다.

gint gtk_combo_box_get_active (GtkComboBox *combobox);
gooblean gtk_combo_box_get_active_iter (GtkComboBox *combobox,
        GtkTreeIter *iter);


첫 번째 함수 gtk_combo_box_get_active()는 현재 행의 색인을 참조하는 정수를 리턴하고, 선택내용이 없을 경우 -1을 리턴한다. 이는 문자열로 변환된 후 다시 GtkTreePath로 변환될 수 있다. 또 gtk_combo_box_get_active_iter() 함수를 이용하면 선택된 행을 가리키는 반복자를 검색하는데, 반복자가 설정되었다면 TRUE를 리턴할 것이다.


다중 행 제거하기

다음 단계는 리스트에서 제품을 제거하는 기능을 추가하는 작업이다. 다중 행의 선택 기능을 추가하였기 때문에 코드는 하나 이상의 행을 제거할 수도 있어야 한다.


리스팅 8-7은 두 개의 함수를 구현한다. 첫 번째 함수 remove_row()는 선택된 행마다 호출되어 범주가 아닐 경우 행을 제거한다. 제거된 행이 만일 구매해야 하는 제품에 해당하면 그 수량이 범주의 실행 중인 총 수량에서 제거된다. 두 번째 함수 remove_products()는 GTK_STOCK_REMOVE 버튼을 클릭하면 실행되는 콜백 함수다.


리스팅 8-7. 하나 또는 이상의 제품 제거하기 (selections.c)

static void
remove_row (GtkTreeRowReference *ref,
        GtkTreeModel *model)
{
    GtkTreeIter parent, iter;
    GtkTreePath *path;
    gboolean buy;
    gint quantity, pnum;

    /* Convert the tree row reference to a path and retrieve the iterator. */
    path = gtk_tree_row_reference_get_path (ref);
    gtk_tree_model_get_iter (model, &iter, path);

    /* Only remove the row if it is not a root row. */
    if (gtk_tree_model_iter_parent (model, &parent, &iter))
    {
        gtk_tree_model_get (model, &iter, BUY_IT, &buy, QUANTITY, &quantity, -1);
        gtk_tree_model_get (model, &parent, QUANTITY, &pnum, -1);

        if (buy)
        {
            pnum -= quantity;
            gtk_tree_store_set (GTK_TREE_STORE (model), &parent, QUANTITY, pnum, -1);
        }

        gtk_tree_model_get_iter (model, &iter, path);
        gtk_tree_store_remove (GTK_TREE_STORE (model), &iter);
    }
}

static void
remove_products (GtkButton *remove,
        GtkTreeView *treeview)
{
    GtkTreeSelection *selection;
    GtkTreeRowReference *ref;
    GtkTreeModel *model;
    GList *rows, *ptr, *references = NULL;

    selection = gtk_tree_view_get_selection (treeview);
    model = gtk_tree_view_get_model (treeview);
    rows = gtk_tree_selection_get_selected_rows (selection, &model);

    /* Create tree row references to all of the selected rows. */
    ptr = rows;
    while (ptr != NULL)
    {
        ref = gtk_tree_row_reference_new (model, (GtkTreePath*) ptr->data);
        references = g_list_prepend (references, gtk_tree_row_reference_copy (ref));
        gtk_tree_row_reference_free (ref);
        ptr = ptr->next;
    }

    /* Remove each of the selected rows pointed to by the row reference. */
    g_list_foreach (references, (GFunc) remove_row, model);

    /* Free the tree paths, tree row references and lists. */
    g_list_foreach (references, (GFunc) gtk_tree_row_reference_free, NULL);
    g_list_foreach (rows, (GFunc) gtk_tree_path_free, NULL);
    g_list_free (references);
    g_list_free (rows);
}


GTK_STOCK_REMOVE 버튼을 누르면 remove_products()가 호출될 것이다. 이 함수는 선택된 행을 가리키는 세 개의 경로로 구성된 이중 연결 리스트를 검색하기 위해 gtk_tree_selection_get_selected_rows()를 호출함으로써 시작된다. 애플리케이션은 행을 변경(alter)할 것이기 때문에 경로 리스트는 행 참조의 리스트로 변환된다. 그래야만 모든 트리 경로가 유효하게 남도록 확보할 수 있다.


Gtkd note.png gtk_tree_selection_selected_foreach()는 행이 변경될 때 사용되어선 안 되기 때문에 이 애플리케이션에서는 사용할 수 없음을 기억하라! 트리 모델이 변경되어 반복자가 예기치 못하게 무효해질 경우 골치가 아파질 것이니 꼭 기억하도록 한다.


경로가 트리 행 참조로 변환되고 나면 g_list_foreach()를 이용해 항목마다 remove_row()를 호출한다. remove_row() 내에서는 새로운 함수가 사용되어 행이 범주인지 확인한다.


선택된 행이 범주일 경우 그것이 루트 요소가 되어 부모를 갖지 않을 것임을 이해할 것이다. 따라서 잇따라 gtk_tree_model_iter_parent()를 호출하면 두 가지 작업을 실행한다. 첫째, parent 반복자가 설정되지 않은 경우 이 함수는 FALSE를 리턴하고, 범주 행이 제거되지 않는다. 행에 부모가 있으면 그것은 제품이라는 뜻으로, parent 반복자가 설정되어 함수에서 추후에 사용될 것이다.

if (gtk_tree_model_iter_parent (model, &parent, &iter))


둘째, 함수는 선택된 제품과 그 부모 범주에 관한 정보를 검색한다. 제품이 구매 제품으로 설정되었다면 범주가 표시한 총 제품 수량에서 그 수량을 제해야 한다. 이 데이터를 변경하면 반복자를 무효화하므로 경로는 반복자로 변환되고 행이 트리 모델에서 제거된다.


더블 클릭 처리하기

더블 클릭은 GtkTreeView의 row-activated 시그널을 이용해 처리된다. 시그널은 사용자가 행을 더블 클릭하거나, 사용자가 스페이스바, Shift-스페이스바, Return, 편집 불가한 행에서 Enter를 누를 때, 혹은 gtk_tree_view_row_activated()를 호출할 때 발생한다.


리스팅 8-8. 클릭된 열 편집하기

static void
row_activated (GtkTreeView *treeview,
        GtkTreePath *path,
        GtkTreeViewColumn *column,
        gpointer data)
{
    GtkTreeModel *model;
    GtkTreeIter iter;

    model = gtk_tree_view_get_model (treeview);
    if (gtk_tree_model_get_iter (model, &iter, path))
    {
        /* Handle the selection ... */
    }
}


리스팅 8-8에서 사용자가 트리 내에서 행을 활성화하면 콜백 함수 row_activated()가 호출된다. 활성화된 행은 gtk_tree_model_get_iter()를 이용해 트리 경로 객체로부터 검색된다. 여기부터 개발자는 행의 내용을 검색하거나 수정하는 데에 지금까지 학습했던 함수들을 원하는 대로 이용하면 되겠다.


편집 가능한 텍스트 렌더러

사용자가 트리 뷰의 내용을 편집하도록 허용한다면 매우 유용하겠다. 이는 사용자가 셀의 내용을 편집할 수 있는 GtkEntry를 포함하는 대화상자를 제시함으로써 이루어진다. 하지만 GTK+는 GtkCellRendererText의 edited 시그널을 이용함으로써 텍스트 구성요소(textual component)를 편집할 수 있는 훨씬 더 간단한 방법을 트리 셀로 통합시켜 제공한다.


사용자가 선택된 행에서 편집 가능하다고 표시된 셀을 클릭하면 셀의 현재 내용을 포함하는 GtkEntry가 셀에 위치될 것이다. 편집되는 셀의 예는 그림 8-8에서 확인할 수 있다.

그림 8-8. 편집 가능한 셀


사용자가 Enter 키를 누르거나 텍스트 엔트리에서 포커스를 제거하면 edited 위젯이 발생할 것이다. 개발자는 시그널이 방출되고 나면 시그널로 연결하여 변경내용을 적용해야 한다. 리스팅 8-9는 제품 열의 편집 가능한 GtkListStore Grocery List 애플리케이션을 생성하는 방법을 보여준다.


리스팅 8-9. 셀의 테스트 편집하기 (editable.c)

static void
setup_tree_view (GtkWidget *treeview)
{
    GtkCellRenderer *renderer;
    GtkTreeViewColumn *column;

    renderer = gtk_cell_renderer_text_new ();
    column = gtk_tree_view_column_new_with_attributes
        ("Buy", renderer, "text", BUY_IT, NULL);
    gtk_tree_view_append_column (GTK_TREE_VIEW (treeview), column);

    renderer = gtk_cell_renderer_text_new ();
    column = gtk_tree_view_column_new_with_attributes
        ("Count", renderer, "text", QUANTITY, NULL);
    gtk_tree_view_append_column (GTK_TREE_VIEW (treeview), column);

    /* Set up the third column in the tree view to be editable. */
    renderer = gtk_cell_renderer_text_new ();
    g_object_set (renderer, "editable", TRUE, "editable-set", TRUE, NULL);

    g_signal_connect (G_OBJECT (renderer), "edited",
        G_CALLBACK (cell_edited),
        (gpointer) treeview);

    column = gtk_tree_view_column_new_with_attributes
        ("Product", renderer, "text", PRODUCT, NULL);
    gtk_tree_view_append_column (GTK_TREE_VIEW (treeview), column);
}

/* Apply the changed text to the cell if it is not an empty string. */
static void
cell_edited (GtkCellRendererText *renderer,
        gchar *path,
        gchar *new_text,
        GtkTreeView *treeview)
{
    GtkTreeIter iter;
    GtkTreeModel *model;

    if (g_ascii_strcasecmp (new_text, "") != 0)
    {
        model = gtk_tree_view_get_model (treeview);
        if (gtk_tree_model_get_iter_from_string (model, &iter, path))
            gtk_list_store_set (GTK_LIST_STORE (model), &iter, PRODUCT, new_text, -1);
    }
}


편집 가능한 GtkCellRendererText 셀은 매우 간단한 과정이다. 가장 먼저 해야 할 일은 텍스트 렌더러의 editable과 editable-set 프로퍼티를 TRUE로 설정하는 것이다.

g_object_set (renderer, "editable", TRUE, "editable-set", TRUE, NULL);


g_object_set()을 editable 프로퍼티로 설정하면 렌더러가 그리는 데이터의 전체 열로 적용될 것임을 기억한다. 셀의 편집 가능성 여부를 행별로 명시하고 싶다면 열의 속성으로 추가해야 한다.


그 다음은 GtkCellRendererText가 제공한 edited 시그널로 셀 렌더러를 연결해야 한다. 해당 시그널에 대한 콜백 함수는 셀 렌더러, 편집된 행을 가리키는 GtkTreePath, 그리고 사용자가 입력한 새로운 텍스트를 수신한다. 이 시그널은 셀이 편집되는 동안 사용자가 셀의 GtkEntry로부터 포커스를 제거하거나 Enter 키를 누를 때 발생한다.


변경내용이 셀로 자동 적용되지 않으면 edited 시그널은 꼭 필요하다. 이는 무효한 엔트리를 필터링하도록 해준다. 예를 들어 리스팅 8-9에서는 새로운 문자열이 비어 있을 때 새로운 텍스트가 적용되지 않는다.

if (gtk_tree_model_get_iter_from_string (model, &iter, path))
    gtk_list_store_set (GTK_LIST_STORE (model), &iter, PRODUCT, new_text, -1);


텍스트를 적용할 준비가 되면 gtk_tree_model_get_iter_from_string()을 이용해 GtkTreePath 문자열을 곧바로 GtkTreeIter로 변환할 수 있다. 반복자가 성공적으로 설정되면 해당 함수는 TRUE를 리턴하는데, 경로 문자열이 유효한 행을 가리킨다는 의미다.


Gtkd caution.png GTK+에서 기능을 제공하긴 하지만 콜백 함수가 초기화된 다음에 행이 제거되거나 이동할 가능성이 있으므로 개발자도 항상 경로가 유효한지 확인해야 할 것이다.


GtkTreeIter를 검색한 후에는 gtk_list_store_set()을 이용해 새로운 문자열을 열로 적용할 수 있다. 리스팅 8-9에서는 GtkListStore의 PRODUCT 열로 new_text가 적용되었다.


셀 데이터 함수

셀을 화면으로 렌더링하기 전에 모든 셀을 맞춤설정하고 싶다면 셀 데이터 함수를 이용할 수 있다. 이러한 함수들은 각 셀의 모든 프로퍼티를 이리저리 변경할 수 있도록 해준다. 가령 셀의 내용을 바탕으로 전경색을 설정한다거나, 표시되는 부동 소수점 수의 소수자리를 제한하는 것이 가능하다. 또 런타임 시 계산되는 프로퍼티를 설정하는 수도 있다.


그림 8-9는 셀 데이터 함수를 이용해 GtkCellRendererText의 text 프로퍼티를 기반으로 각 셀의 배경색을 설정하는 애플리케이션을 보여준다.

그림 8-9. 색상 리스트를 생성하는 리스팅 8-10의 스크린샷


Gtkd caution.png 자신의 트리 모델에 행의 수가 많다면 셀 데이터 함수를 사용하지 말라. 셀 데이터 함수는 셀이 렌더링되기 전에 열 내의 모든 행을 처리하므로 행의 개수가 많은 트리 모델은 속도가 급격히 저하될 수 있다.


리스팅 8-10에서는 셀 데이터 함수를 이용하여 색상을 셀이 저장한 색상 문자열의 값으로 설정하였다. 전경색 또한 모든 셀에 흰색으로 설정되었지만 g_object_set()을 이용해 전체 렌더러로 적용이 가능하다. 해당 애플리케이션은 256개의 웹 안전색상(web-safe colors) 리스트를 보여준다.


리스팅 8-10. 셀 데이터 함수 이용하기 (celldatafunctions.c)

#include <gtk/gtk.h>

enum
{
    COLOR = 0,
    COLUMNS
};

const gchar *clr[6] = { "00", "33", "66", "99", "CC", "FF" }; 

static void setup_tree_view (GtkWidget*);
static void cell_data_func (GtkTreeViewColumn*, GtkCellRenderer*,
        GtkTreeModel*, GtkTreeIter*, gpointer);

int main (int argc,
        char *argv[])
{
    GtkWidget *window, *treeview, *scrolled_win;
    GtkListStore *store;
    GtkTreeIter iter;
    guint i, j, k;

    gtk_init (&argc, &argv);

    window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
    gtk_window_set_title (GTK_WINDOW (window), "Color List");
    gtk_container_set_border_width (GTK_CONTAINER (window), 10);
    gtk_widget_set_size_request (window, 250, 175);

    treeview = gtk_tree_view_new ();
    setup_tree_view (treeview);
    store = gtk_list_store_new (COLUMNS, G_TYPE_STRING);

    /* Add all of the products to the GtkListStore. */
    for (i = 0; i < 6; i++)
        for (j = 0; j < 6; j++)
            for (k = 0; k < 6; k++)
            {
                gchar *color = g_strconcat ("#", clr[i], clr[j], clr[k], NULL);
                gtk_list_store_append (store, &iter);
                gtk_list_store_set (store, &iter, COLOR, color, -1);
                g_free (color);
            }

    gtk_tree_view_set_model (GTK_TREE_VIEW (treeview), GTK_TREE_MODEL (store));
    g_object_unref (store);

    scrolled_win = gtk_scrolled_window_new (NULL, NULL);
    gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_win),
        GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);

    gtk_container_add (GTK_CONTAINER (scrolled_win), treeview);
    gtk_container_add (GTK_CONTAINER (window), scrolled_win);
    gtk_widget_show_all (window);

    gtk_main ();
    return 0;
}

/* Add three columns to the GtkTreeView. All three of the columns will be
* displayed as text, although one is a gboolean value and another is
* an integer. */
static void
setup_tree_view (GtkWidget *treeview)
{
    GtkCellRenderer *renderer;
    GtkTreeViewColumn *column;

    renderer = gtk_cell_renderer_text_new ();
    column = gtk_tree_view_column_new_with_attributes

        ("Standard Colors", renderer, "text", COLOR, NULL);
    gtk_tree_view_append_column (GTK_TREE_VIEW (treeview), column);

    gtk_tree_view_column_set_cell_data_func (column, renderer,
        cell_data_func, NULL, NULL);
}

static void
cell_data_func (GtkTreeViewColumn *column,
        GtkCellRenderer *renderer,
        GtkTreeModel *model,
        GtkTreeIter *iter,
        gpointer data)
{
    gchar *text;

    /* Get the color string stored by the column and make it the foreground color. */
    gtk_tree_model_get (model, iter, COLOR, &text, -1);
    g_object_set (renderer, "foreground", "#FFFFFF", "foreground-set", TRUE,
        "background", text, "background-set", TRUE, "text", text, NULL);
    g_free (text);
}


셀 데이터 함수가 유용하게 사용되는 또 다른 예로 부동 소수점 수를 이용하여 표시되어야 할 소수 자리수를 제어해야 하는 경우를 들 수 있겠다. 사실 이와 관련된 예는 본 장의 "스핀 버튼 셀 렌더러"에서 스핀 버튼 셀 런더러를 학습 시 사용할 것이다.


셀 데이터 함수를 준비하고 나면 gtk_tree_view_column_set_cell_data_func()를 호출하여 특정 열로 연결해야 한다. 해당 함수의 마지막 두 매개변수는 셀 데이터 함수로 전달될 데이터와 데이터를 소멸하도록 호출되는 추가 함수를 제공하도록 해준다. 불필요하다면 두 매개변수 모두 NULL로 설정 가능하다.

void gtk_tree_view_column_set_cell_data_func (GtkTreeViewColumn *column,
        GtkCellRenderer *renderer,
        GtkTreeCellDataFunc cell_data_func,
        gpointer data,
        GtkDestroyNotify destroy_data);


제거하길 원하는 셀 데이터 함수를 열로 추가했다면 cell_data_func 매개변수를 NULL로 설정하여 gtk_tree_view_column_set_cell_data_func()를 호출해야 한다.


앞서 언급하였듯 셀 데이터 함수는 데이터의 렌더링을 미세하게 조정(fine-tuning)해야 할 필요가 있을 때에만 사용해야 한다. 대부분의 경우는 설정의 범위에 따라 추가적인 열 속성이나 g_object_set()의 사용을 통해 프로퍼티를 변경해도 충분하다. 경험상 말하건대 셀 데이터 함수는 열 속성으로 처리할 수 없거나 모든 셀에 적용할 수 없는 설정을 적용 시에만 사용되어야 한다.


셀 렌더러

지금까지는 셀 렌더러의 한 가지 타입, GtkCellRendererText를 학습하였다. 이 렌더러는 문자열, 숫자, Boolean 값을 텍스트로 표시하도록 해준다. 개발자는 셀 렌더러 속성과 셀 데이터 함수를 이용해 텍스트를 어떻게 표시할 것인지 맞춤설정이 가능할 뿐만 아니라 사용자에 의한 편집도 허용한다.


GTK+는 텍스트 외에도 여러 타입의 위젯을 표시할 수 있는 수많은 셀 렌더러를 제공한다. 여기에는 토글 버튼, 이미지, 스핀 버튼, 콤보 박스, 진행 막대, 가속기가 포함되며, 이번 장에서는 이들을 살펴볼 것이다.


토글 버튼 렌더러

GtkCellRendererText를 이용해 Boolean 값을 "TRUE" 또는 "FALSE"로 표시하는 일은 약간 엉성할 뿐더러 특히 시각적 Boolean 열이 많을 때에는 각 행의 중요한 공간을 많이 차지하기도 한다. 따라서 Boolean 값에 대해 텍스트 문자열 대신 체크 버튼을 표시한다면 좋겠단 생각이 들 것이다. 좋은 소식은 GtkCellRendererToggle이라는 셀 렌더러 타입의 도움을 약간 빌린다면 체크 버튼의 사용이 가능하단 사실이다.


기본적으로 토글 버튼 셀 렌더러는 그림 8-10에서처럼 체크 버튼으로 그려진다. 토글 버튼 렌더러를 라디오 버튼처럼 그릴 수도 있지만 그런 경우 라디오 버튼 기능을 개발자가 알아서 관리해야 할 것이다.

그림 8-10. 토글 버튼 렌더러


편집 가능한 텍스트 렌더러와 마찬가지로 개발자는 사용자가 실행하는 변경내용을 수동으로 적용해야 한다. 그렇지 않으면 버튼이 화면에서 시각적으로 토글되지 않을 것이다. 이 때문에 GtkCellRendererToggle은 toggled 시그널을 제공하는데, 이는 사용자가 체크 버튼을 누르면 발생된다. 리스팅 8-11은 Grocery List 애플리케이션에 대한 toggled 콜백 함수를 제시한다. 이러한 애플리케이션 버전에서 BUY_IT 열은 GtkCellRendererToggle을 이용해 렌더링된다.


리스팅 8-11. GtkCellRendererToggle 의 Toggled 콜백 함수

static void
buy_it_toggled (GtkCellRendererToggle *renderer,
        gchar *path,
        GtkTreeView *treeview)
{
    GtkTreeModel *model;
    GtkTreeIter iter;
    gboolean value;

    /* Toggle the cell renderer's current state to the logical not. */
    model = gtk_tree_view_get_model (treeview);
    if (gtk_tree_model_get_iter_from_string (model, &iter, path))
    {
        gtk_tree_model_get (model, &iter, BUY_IT, &value, -1);
        gtk_list_store_set (GTK_LIST_STORE (model), &iter, BUY_IT, !value, -1);
    }
}

gtk_tree_model_get (model, &iter, BUY_IT, &value, -1);
gtk_list_store_set (GTK_LIST_STORE (model), &iter, BUY_IT, !value, -1);


토글 셀 렌더러는 gtk_cell_renderer_toggle_new()를 이용해 생성된다. 토글 셀 렌더러를 생성한 후에는 그것의 activatable 프로퍼티를 TRUE로 설정해야만 토글이 가능해진다. 그렇지 않으면 사용자는 버튼을 토글할 수 없게 된다 (설정을 표시하기만 하고 편집은 허용하지 않을 때 유용하겠다). g_object_set()을 이용하면 이 설정을 모든 셀로 적용할 수 있다.


다음으로 GtkCellRendererText가 사용했던 text 대신 active 프로퍼티를 열 속성으로 추가해야 한다. 해당 프로퍼티는 원하는 토글 버튼의 상태에 따라 TRUE 또는 FALSE로 설정된다.


이후 개발자는 toggled 시그널을 위해 GtkCellRendererToggle 셀 렌더러를 콜백 함수로 연결해야 한다. 리스팅 8-11은 toggled 시그널에 대한 콜백 함수의 예를 제공한다. 해당 콜백 함수는 토글 버튼을 포함하는 행을 가리키는 GtkTreePath 문자열과 셀 렌더러를 수신한다.


콜백 함수 내에서는 아래 두 줄에 표시된 바와 같이 토글 버튼이 표시하는 현재 값을 수동으로 토글해야 한다. toggled 시그널의 발생은 사용자가 버튼의 토글을 원한다는 사실만 알려줄 뿐, 알아서 액션을 실행하진 않는다.

gtk_tree_model_get (model, &iter, BUY_IT, &value, -1);
gtk_list_store_set (GTK_LIST_STORE (model), &iter, BUY_IT, !value, -1);


값을 토글하기 위해서는 셀이 저장한 현재 값을 검색하도록 gtk_tree_model_get()을 이용할 수 있다. 셀은 Boolean 값을 저장할 것이기 때문에 gtk_list_store_set()에서 새로운 값을 현재 값의 반대로 설정할 수 있다.


앞서 언급했듯이 GtkCellRendererToggle은 토글을 라디오 버튼으로 렌더링하도록 허용하기도 한다. 이는 gtk_cell_renderer_toggle_set_radio()를 이용해 radio 프로퍼티를 변경함으로써 초기에 렌더러로 설정이 가능하다.

void gtk_cell_renderer_toggle_set_radio (GtkCellRendererToggle *toggle,
        gboolean radio);


radio 프로퍼티를 TRUE로 설정하면 토글 버튼의 렌더링밖에 달라지는 것이 없다는 사실도 인식해야 한다! 개발자는 자신의 toggled 콜백 함수를 통해 라디오 버튼의 기능을 수동으로 구현해야 할 것이다. 새로운 토글 버튼을 활성화하고 이전에 선택된 토글 버튼을 비활성화하는 작업도 이에 포함된다.


Pixbuf 렌더러

Gdkixbuf 객체의 형태로 된 이미지를 GtkTreeView에서 열로 추가하는 기능은 GtkCellRendererPixbuf가 제공하는 매우 유용한 기능에 해당한다. Pixbuf 렌더러의 예제는 그림 8-11에서 확인 가능한데, 그림을 보면 각 항목의 좌측에 작은 아이콘이 표시되어 있다.

그림 8-11. Pixbuf 렌더러


GdkPixbuf 이미지를 트리로 추가하는 데 필요한 내용은 거의 앞 부분에서 학습했겠지만 리스팅 8-12에 실린 간단한 예를 통해 지침을 제공하겠다. 대부분의 경우 pixbuf에 대해 구분된 열 헤더를 제공할 필요가 없기 때문에 리스팅 8-12는 다수의 렌더러를 하나의 열로 포함시키는 방법을 보여줄 것이다. Pixbuf 셀 렌더러는 파일 시스템 브라우저와 같은 트리 뷰 구현의 유형에 매우 유용하다.


리스팅 8-12. GdkPixbuf 셀 렌더러

static void
setup_tree_view (GtkWidget *treeview)
{
    GtkCellRenderer *renderer;
    GtkTreeViewColumn *column;

    /* Create a tree view column with two renderers, one a pixbuf and one text. */
    column = gtk_tree_view_column_new ();
    gtk_tree_view_column_set_title (column, "Products");

    renderer = gtk_cell_renderer_pixbuf_new ();
    gtk_tree_view_column_pack_start (column, renderer, FALSE);
    gtk_tree_view_column_set_attributes (column, renderer, "pixbuf", ICON, NULL);

    renderer = gtk_cell_renderer_text_new ();
    gtk_tree_view_column_pack_start (column, renderer, TRUE);
    gtk_tree_view_column_set_attributes (column, renderer, "text", PRODUCT, NULL);

    gtk_tree_view_append_column (GTK_TREE_VIEW (treeview), column);
}


gtk_cell_renderer_pixbuf_new()를 이용해 새로운 GtkCellRendererPixbuf 객체가 생성되었다. 개발자는 이제 렌더러를 열로 추가하길 원할 것이다. 열에는 다수의 렌더러가 존재할 것이기 때문에 렌더러를 열로 추가하려면 gtk_tree_view_column_pack_start()를 이용할 수 있다.


다음으로 GtkCellRendererPixbuf 객체에 대한 속성을 열로 추가해야 한다. 리스팅 8-12에서는 pixbuf 프로퍼티를 이용해 파일로부터 커스텀 아이콘을 로딩할 수 있도록 하였다. 하지만 pixbuf는 GtkCellRendererPixbuf가 지원하는 유일한 이미지 타입은 아니다. 스톡 아이콘 식별자를 제공하도록 stock-id 프로퍼티를 이용할 수도 있다. 이는 커스텀 GdkPixbuf 이미지 대신 스톡 아이콘을 표시할 것이다. GTK+ 2.10 이후 버전에서 이용 가능한 스톡 아이콘의 전체 목록은 부록 D에서 찾을 수 있다.


GtkTreeStore를 이용 중이라면 행이 추가되거나 제거(retract)될 때 다른 pixbuf를 표시할 수 있다면 유용하겠다. 이를 위해 pixbuf-expander-open 과 pixbuf-expander-closed에 두 개의 GdkPixbuf 객체를 명시할 수 있다. 가령 행이 확장되면 open 폴더를 표시하고 행이 제거되면 closed 폴더를 표시할 때 이 방법을 이용할 수 있겠다.


트리 모델을 생성할 때는 GDK_TYPE_PIXBUF라고 불리는 새로운 타입을 이용해야 하는데, 이 함수는 각 모델 열에 GdkPixbuf 객체를 저장할 것이다. 트리 모델 열로 GdkPixbuf를 추가할 때마다 그 참조 계수는 1씩 증가한다. 사용이 끝나면 GdkPixbuf 객체에서 g_object_unref()를 호출하여 트리 뷰와 동시에 소멸되도록 해야 한다.


스핀 버튼 렌더러

제 4장에서 GtkSpinButton 위젯의 사용 방법을 학습한 바 있다. GtkCellRendererText도 숫자를 표시할 수는 있지만 GtkCellRendererSpin을 사용하는 편이 더 낫다. 내용이 편집되면 GtkEntry를 표시하는 대신 GtkSpinButton이 사용된다. 편집 중인 셀을 GtkCellRendererSpin을 이용해 렌더링하는 예제를 그림 8-12에 표시하였다.

그림 8-12. 스핀 버튼 렌더러


그림 8-12의 첫 열에 부동 소수점 수에 여러 개의 소수 자리가 표시됨을 눈치챌 것이다. 스핀 버튼에 표시되는 소수 자리수는 설정이 가능하지만 표시되는 텍스트는 설정할 수 없다. 소수 자리수를 감소시키거나 제거하려면 셀 데이터 함수를 이용해야 한다. 소수 자리수를 숨기는 셀 데이터 함수의 예를 리스팅 8-13에 소개하겠다.


리스팅 8-13. 부동 소수점 수에 대한 셀 데이터 함수

static void
cell_data_func (GtkTreeViewColumn *column,
        GtkCellRenderer *renderer,
        GtkTreeModel *model,
        GtkTreeIter *iter,
        gpointer data)
{
    gfloat value;
    gchar *text;

    /* Retrieve the current value and render it with no decimal places. */
    gtk_tree_model_get (model, iter, QUANTITY, &value, -1);
    text = g_strdup_printf ("%.0f", value);
    g_object_set (renderer, "text", text, NULL);
    g_free (text);
}


GtkCellRendererText 또는 그 외의 파생된 렌더러를 이용해 열에서 부동 소수점 수가 표시한 소수 자리수를 지시하고 싶다면 셀 데이터 함수를 사용해야 함을 학습한 바 있다. 리스팅 8-13에서 보인 셀 데이터 함수의 예제는 현재 부동 소수점 수를 읽고 렌더러에게 강제로 0개의 소수 자리를 표시하도록 지시한다. GtkCellRendererSpin은 숫자를 부동 소수점 수로 저장하기 때문에 이는 꼭 필요하다.


GtkCellRendererSpin은 정수 및 부동 소수점 수와 모두 호환되는데, 그 매개변수가 GtkAdjustment에 저장되기 때문이다. 리스팅 8-14는 Grocery List 애플리케이션의 구현으로, Quantity 열은 GtkCellRendererSpin을 이용해 렌더링된다.


리스팅 8-14. 스핀 버튼 셀 렌더러

static void
setup_tree_view (GtkWidget *treeview)
{
    GtkCellRenderer *renderer;
    GtkTreeViewColumn *column;
    GtkAdjustment *adj;

    adj = GTK_ADJUSTMENT (gtk_adjustment_new (0.0, 0.0, 100.0, 1.0, 2.0, 2.0));

    renderer = gtk_cell_renderer_spin_new ();
    g_object_set (renderer, "editable", TRUE, "adjustment", adj, "digits", 0, NULL);

    g_signal_connect (G_OBJECT (renderer), "edited",
        G_CALLBACK (cell_edited),
        (gpointer) treeview);

    column = gtk_tree_view_column_new_with_attributes
        ("Count", renderer, "text", QUANTITY, NULL);
    gtk_tree_view_append_column (GTK_TREE_VIEW (treeview), column);

    /* ... Add a cell renderer for the PRODUCT column ... */
}

/* Apply the changed text to the cell. */
static void
cell_edited (GtkCellRendererText *renderer,
        gchar *path,
        gchar *new_text,
        GtkTreeView *treeview)
{
    GtkTreeIter iter;
    GtkTreeModel *model;
    GtkAdjustment *adjustment;
    gdouble value;

    /* Retrieve the current value stored by the spin button renderer's adjustment. */
    g_object_get (renderer, "adjustment", &adjustment, NULL);
    value = gtk_adjustmnet_get_value (adjustment);

    model = gtk_tree_view_get_model (treeview);
    if (gtk_tree_model_get_iter_from_string (model, &iter, path))
        gtk_list_store_set (GTK_LIST_STORE (model), &iter, QUANTITY, value, -1);
}


새로운 GtkCellRendererSpin 객체는 gtk_cell_renderer_spin()을 이용해 생성된다. 렌더러를 생성한 후에는 g_object_set()을 이용해 객체의 editable, adjustment, digits 프로퍼티를 설정해야 한다.

g_object_set (renderer, "editable", TRUE, "adjustment", adj, "digits", 0, NULL);


GtkCellRendererSpin은 세 가지 프로퍼티, adjustment, climb-rate, digits를 제공한다. 이들은 GtkAdjustment에 저장되며, 각각 스핀 버튼의 프로퍼티, 화살표 버튼을 누르고 있을 때 가속률(acceleration rate), 스핀 버튼에 표시할 소수 자리수를 정의한다. 이동 속도(climb rate)와 표시할 소수 자리수는 기본값 0으로 설정된다.


GtkCellRendererSpin은 GtkCellRendererText에서 파생되므로, 셀 내용의 편집하려면 TRUE로 설정되어야 하는 editable을 포함해 모든 GtkCellRendererText의 프로퍼티를 이용할 수 있다.


셀 렌더러를 준비한 후에는 edited 시그널을 셀 렌더러로 연결해야 하는데, 이는 후에 사용자가 선택한 새로운 값을 셀로 적용하는 데 사용될 것이다. 보통은 이 값을 필터링할 필요가 없는데, 조정(adjustment)이 이미 셀이 허용하는 값을 제한하기 때문이다. 콜백 함수는 사용자가 Enter 키를 누르거나 편집 중인 셀의 스핀 버튼에서 포커스를 이동시킨 다음에 실행될 것이다.


리스팅 8-14의 cell_edited() 콜백 함수 내에서는 먼저 스핀 버튼 렌더러의 조정을 검색해야 하는데, 표시되어야 할 새로운 값을 저장하기 때문이다. 검색한 후 새 값은 주어진 셀로 적용 가능하다.


Gtkd note.png GtkCellRendererText의 edited 시그널은 여전히 new_text 매개변수를 수신하지만 이를 사용해선 안 된다. 매개변수는 스핀 버튼의 값에 대한 텍스트 버전을 저장하지 않을 것이다. 뿐만 아니라 현재값을 대체하게 될 gtk_list_store_set()에 사용된 값은 부동 소수점 수로 제공되어야 하므로, 그 내용과 관계 없이 문자열은 허용되지 않는다.


gtk_adjustment_get_value()를 이용하면 조정을 적절한 열로 적용하여 조정의 값을 검색할 수 있다. 부동 소수점 수를 표시하도록 QUANTITY 열이 사용되기 때문에 (G_TYPE_FLOAT) 리턴된 타입은 현재 상태로 사용할 수 있다.


트리 모델을 생성할 때 설사 정수로 저장하고 싶더라도 열의 타입은 G_TYPE_FLOAT이어야 한다. 개발자는 각 셀이 표시하는 소수 자리수를 제한하기 위해 셀 데이터 함수를 이용해야 한다.


콤보 박스 렌더러

GtkCellRendererCombo는 방금 학습한 위젯, GtkComboBox에 대한 셀 렌더러를 제공한다. 콤보 박스 셀 렌더러는 개발자로 하여금 사전에 정의된 옵션을 사용자에게 여러 가지 제공할 수 있도록 해주기 때문에 유용하다. GtkCellRendererCombo는 GtkCellRendererText와 비슷한 방식으로 텍스트를 렌더링하지만 편집 시 사용자에게 GtkEntry 대신 GtkComboBox 위젯이 표시된다. 편집 중인 GtkCellRendererCombo 셀의 모습은 그림 8-13에서 확인할 수 있다.

그림 8-13. 콤보 박스 셀 렌더러


GtkCellRendererCombo를 사용하기 위해서는 열의 모든 셀마다 GtkTreeModel을 생성할 필요가 있다. 리스팅 8-15에서는 리스팅 8-1의 Grocery List 애플리케이션에서 QUANTITY 열을 GtkCellRendererCombo를 이용해 렌더링하였다.


리스팅 8-15. 콤보 박스 셀 렌더러

static void
setup_tree_view (GtkWidget *treeview)
{
    GtkCellRenderer *renderer;
    GtkTreeViewColumn *column;
    GtkListStore *model;
    GtkTreeIter iter;

    /* Create a GtkListStore that will be used for the combo box renderer. */
    model = gtk_list_store_new (1, G_TYPE_STRING);

    gtk_list_store_append (model, &iter);
    gtk_list_store_set (model, &iter, 0, "None", -1);
    gtk_list_store_append (model, &iter);
    gtk_list_store_set (model, &iter, 0, "One", -1);
    gtk_list_store_append (model, &iter);
    gtk_list_store_set (model, &iter, 0, "Half a Dozen", -1);
    gtk_list_store_append (model, &iter);
    gtk_list_store_set (model, &iter, 0, "Dozen", -1);
    gtk_list_store_append (model, &iter);
    gtk_list_store_set (model, &iter, 0, "Two Dozen", -1);

    /* Create the GtkCellRendererCombo and add the tree model. Then, add the
    * renderer to a new column and add the column to the GtkTreeView. */
    renderer = gtk_cell_renderer_combo_new ();
    g_object_set (renderer, "text-column", 0, "editable", TRUE,
        "has-entry", TRUE, "model", model, NULL);
    column = gtk_tree_view_column_new_with_attributes
        ("Count", renderer, "text", QUANTITY, NULL);
    gtk_tree_view_append_column (GTK_TREE_VIEW (treeview), column);

    g_signal_connect (G_OBJECT (renderer), "edited",
        G_CALLBACK (cell_edited),
        (gpointer) treeview);

    renderer = gtk_cell_renderer_text_new ();
    column = gtk_tree_view_column_new_with_attributes
        ("Product", renderer, "text", PRODUCT, NULL);
    gtk_tree_view_append_column (GTK_TREE_VIEW (treeview), column);
}

/* Apply the changed text to the cell. */
static void
cell_edited (GtkCellRendererText *renderer,
        gchar *path,
        gchar *new_text,
        GtkTreeView *treeview)
{
    GtkTreeIter iter;
    GtkTreeModel *model;

    /* Make sure the text is not empty. If not, apply it to the tree view cell. */
    if (g_ascii_strcasecmp (new_text, "") != 0)
    {
        model = gtk_tree_view_get_model (treeview);
        if (gtk_tree_model_get_iter_from_string (model, &iter, path))
            gtk_list_store_set (GTK_LIST_STORE (model), &iter, QUANTITY, new_text, -1);
    }
}


새로운 콤보 박스 셀 렌더러는 gtk_cell_renderer_combo_new()를 이용해 생성된다. GtkCellRendererCombo는 GtkRendererText: has-entry, model, text-column에서 상속된 프로퍼티 외에도 세 가지 프로퍼티를 포함한다.

g_object_set (renderer, "text-column", 0, "editable", TRUE,
        "has-entry", TRUE, "model", model, NULL);


가장 먼저 설정해야 하는 프로퍼티는 콤보 박스의 트리 모델 중에서 셀 렌더러에 표시될 열을 나타내는 text-column이다. 이는 G_TYPE_STRING, G_TYPE_INT, 또는 G_TYPE_BOOLEAN과 같이 GtkCellRendererText가 지원하는 타입이어야 한다. model 프로퍼티는 GtkTreeModel로, 콤보 박스의 내용으로 사용될 것이다. editable 프로퍼티도 TRUE로 설정해야만 셀 내용이 편집 가능해진다.


마지막으로 사용자에게 일반 콤보 박스와 같은 선택권을 제공하지만 기존의 옵션을 선택하는 대신 GtkEntry 위젯을 이용해 커스텀 문자열을 사용자가 입력하도록 허용하는 GtkComboBoxEntry라는 위젯이 있다. 콤보 박스 셀 렌더러에서 이 기능의 사용을 허용하려면 has-entry 프로퍼티를 TRUE로 설정해야 한다. 이 기능은 기본적으로 켜져 있는데, GtkCellRendererCombo의 트리 모델에 표시되는 항목으로 선택을 제한하려면 기능을 꺼야 한다.


GtkCellRendererText에서 파생된 여느 셀 렌더러와 마찬가지로 text 필드를 열 속성으로 이용하고 트리 뷰의 모델을 생성할 때 초기 텍스트를 설정하길 원할 것이다. 그리고 나면 edited 시그널을 이용해 텍스트를 트리 모델로 적용할 수 있다. 리스팅 8-15에서는 사용자가 자유 형식의 텍스트도 마음껏 입력할 수 있기 때문에 new_text 문자열이 비어 있지 않을 경우에만 변경내용이 적용된다.


진행 막대 렌더러

또 다른 타입의 셀 렌더러로 GtkCellRendererProgress가 있는데, 이는 GtkProgressBar 위젯을 구현한다. 진행 막대는 펄싱(pulsing)을 지원하지만 GtkCellRendererProgress에는 진행 막대의 현재 값을 설정하도록 허용하는 기능만 있다. 그림 8-14는 텍스트로 된 피드백이 표시된 두 번째 열에 진행 막대 셀 렌더러를 가진 GtkTreeView 위젯의 모습을 실었다.

그림 8-14. 진행 막대 셀 렌더러


진행 막대 셀 렌더러는 프로그램에서 구현하기 쉬운 기능 중 하나다. 새로운 GtkCellRendererProgress 객체를 생성하려면 gtk_cell_renderer_progress_new()를 이용한다. GtkCellRendererProgress는 두 가지 프로퍼티, text와 value를 제공한다.


진행 막대 상태는 value 프로퍼티에서 정의되는데, 이는 0에서 100까지 값으로 된 정수다. value가 0이면 빈 진행 막대를 의미하고, 100이면 꽉 찬 진행 막대를 가리킨다. 정수로 저장되므로 진행 막대의 값에 상응하는 트리 모델 행은 G_TYPE_INT 타입이어야 한다.


GtkCellRendererProgress가 제공하는 두 번째 프로퍼티는 text다. 이 프로퍼티는 진행 막대 위에 그려지는 문자열이다. 해당 프로퍼티는 일부 경우에서는 무시되지만 프로세스 진행 상황에 대한 정보를 사용자에게 제공하는 것은 언제든 환영이다. 진행 막대에 가능한 문자열은 "67% Complete", "3 of 80 Files Processed", "Installing foo..." 등이 있다.


GtkCellRendererProgress는 일부 상황에서 유용한 셀 렌더러지만 사용 시 주의를 기울여야 한다. 여러 개의 진행 막대를 하나의 행에 사용하면 수평적 공간을 많이 차지하여 사용자에게 혼란을 일으키므로 이런 상황을 피하도록 한다. 또 행이 많은 트리 뷰는 지저분하게 보인다. 많은 경우에서 진행 막대 셀 렌더러 대신 텍스트로 된 셀 렌더러를 이용하는 편이 사용자에게 더 낫다.


하지만 GtkCellRendererProgress가 더 나은 선택이 되는 상황도 더러 있다. 가령 애플리케이션이 동시에 여러 개의 다운로드를 관리해야 하는 경우에는 진행 막대 셀 렌더러를 이용 시 각 다운로드의 진행에 대해 일관된 피드백을 제공하기 쉽다.


키보드 가속기 렌더러

GTK+ 2.10은 GtkCellRendererAccel이라는 새로운 타입의 셀 렌더러를 도입하였는데, 이는 키보드 가속기에 대한 텍스트 표현을 표시한다. 가속기 셀 렌더러에 대한 예는 그림 8-15에서 확인할 수 있다.

그림 8-15. 가속기 셀 렌더러


리스팅 8-16은 액션에 대한 키보드 가속기와 함께 액션의 리스트를 생성한다. 이러한 타입의 트리 뷰는 사용자가 애플리케이션에 대한 가속기를 편집하도록 허용할 때 사용할 수 있다. 렌더러는 GtkCellRendererText에서 파생되기 때문에 가속기는 텍스트로 표시된다.


가속기를 편집하려면 사용자는 셀을 한 번 클릭한다. 그러면 셀은 키를 요구하는 문자열을 표시할 것이다. 새로운 키 코드와 함께 Ctrl 이나 Shft 와 같은 마스크 키(mask key)가 셀로 추가될 것이다. 기본적으로는 전달되는 첫 번째 키보드 단축키가 셀에 표시될 것이다.


리스팅 8-16. 키보드 가속기 셀 렌더러 (accelerators.c)

#include <gtk/gtk.h>
#include <gdk/gdkkeysyms.h>

enum
{
    ACTION = 0,
    MASK,
    VALUE,
    COLUMNS
};

typedef struct
{
    gchar *action;
    GdkModifierType mask;
    guint value;
} Accelerator;

const Accelerator list[] =
{
    { "Cut", GDK_CONTROL_MASK, GDK_X },
    { "Copy", GDK_CONTROL_MASK, GDK_C },
    { "Paste", GDK_CONTROL_MASK, GDK_V },
    { "New", GDK_CONTROL_MASK, GDK_N },
    { "Open", GDK_CONTROL_MASK, GDK_O },
    { "Print", GDK_CONTROL_MASK, GDK_P },
    { NULL, NULL, NULL }
};

static void setup_tree_view (GtkWidget*);
static void accel_edited (GtkCellRendererAccel*, gchar*, guint,
        GdkModifierType, guint, GtkTreeView*);

int main (int argc,
        char *argv[])
{
    GtkWidget *window, *treeview, *scrolled_win;
    GtkListStore *store;
    GtkTreeIter iter;
    guint i = 0;

    gtk_init (&argc, &argv);

    window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
    gtk_window_set_title (GTK_WINDOW (window), "Accelerator Keys");
    gtk_container_set_border_width (GTK_CONTAINER (window), 10);
    gtk_widget_set_size_request (window, 250, 250);

    treeview = gtk_tree_view_new ();
    setup_tree_view (treeview);

    store = gtk_list_store_new (COLUMNS, G_TYPE_STRING, G_TYPE_INT, G_TYPE_UINT);

    /* Add all of the keyboard accelerators to the GtkListStore. */
    while (list[i].action != NULL)
    {
        gtk_list_store_append (store, &iter);
        gtk_list_store_set (store, &iter, ACTION, list[i].action,
                MASK, (gint) list[i].mask, VALUE, list[i].value, -1);
        i++;
    }

    gtk_tree_view_set_model (GTK_TREE_VIEW (treeview), GTK_TREE_MODEL (store));
    g_object_unref (store);

    scrolled_win = gtk_scrolled_window_new (NULL, NULL);
    gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_win),
        GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);

    gtk_container_add (GTK_CONTAINER (scrolled_win), treeview);
    gtk_container_add (GTK_CONTAINER (window), scrolled_win);
    gtk_widget_show_all (window);

    gtk_main ();
    return 0;
}

/* Create a tree view with two columns. The first is an action and the
* second is a keyboard accelerator. */
static void
setup_tree_view (GtkWidget *treeview)
{
    GtkCellRenderer *renderer;
    GtkTreeViewColumn *column;

    renderer = gtk_cell_renderer_text_new ();
    column = gtk_tree_view_column_new_with_attributes
        ("Buy", renderer, "text", ACTION, NULL);
    gtk_tree_view_append_column (GTK_TREE_VIEW (treeview), column);

    renderer = gtk_cell_renderer_accel_new ();
    g_object_set (renderer, "accel-mode", GTK_CELL_RENDERER_ACCEL_MODE_GTK,
        "editable", TRUE, NULL);

    column = gtk_tree_view_column_new_with_attributes ("Buy", renderer,
        "accel-mods", MASK, "accel-key", VALUE, NULL);
    gtk_tree_view_append_column
 (GTK_TREE_VIEW (treeview), column);

    g_signal_connect (G_OBJECT (renderer), "accel_edited",
        G_CALLBACK (accel_edited),
        (gpointer) treeview);
}

/* Apply the new keyboard accelerator key and mask to the cell. */
static void
accel_edited (GtkCellRendererAccel *renderer,
        gchar *path,
        guint accel_key,
        GdkModifierType mask,
        guint hardware_keycode,
        GtkTreeView *treeview)
{
    GtkTreeModel *model;
    GtkTreeIter iter;

    model = gtk_tree_view_get_model (treeview);
    if (gtk_tree_model_get_iter_from_string (model, &iter, path))
        gtk_list_store_set (GTK_LIST_STORE (model), &iter,
                MASK, (gint) mask, VALUE, accel_key, -1);
}


gtk_cell_renderer_accel_new()를 이용해 새로운 GtkCellRendererAccel 객체를 생성할 수 있다. GtkCellRendererAccel은 g_object_get()을 이용해 접근 가능한 다음 네 가지 프로퍼티를 제공한다.

  • accel-key: 가속기에 해당하는 키 값. 키 값의 전체 목록은 <gdk/gdkkeysyms.h>에서 찾을 수 있다.
  • accel-mode: GtkCellRendererAccelMode 값으로, GTK_CELL_RENDERER_ACCEL_MODE_GTK 또는 GTK_CELL_RENDERER_ACCEL_MODE_OTHER에 해당한다. 이는 셀에서 가속기가 어떻게 렌더링되는지를 정의한다. 보통은 GTK+의 렌더링 버전을 이용해야 한다.
  • accel-mods: GdkModifierType 타입의 가속기 수식키(modifier). 개발자가 Shift, Ctrl, Alt, 다른 마스킹 키(masking key)를 감지하도록 해준다.
  • keycode: 가속기의 하드웨어 키코드로, 자주 사용되지는 않는다. 키 값을 정의하지 않았을 때에만 필요하다.


accel-mods 값은 애플리케이션으로부터 직접적인 액션을 야기하지 않는 키를 감지하도록 해준다. 이러한 값은 GdkModifierType 열거에 의해 정의되지만 키보드 가속기를 다룰 때 모든 값이 발생할 수 있는 것은 아니다. 중요한 값의 리스트는 다음과 같다.

  • GDK_SHIFT_MASK: Shift 키.
  • GDK_CONTROL_MASK: Ctrl 키.
  • GDK_MOD_MASK, GDK_MOD2_MASK, GDK_MOD3_MASK, GDK_MOD4_MASK, GDK_MOD5_MASK: 첫 번째 수식키는 주로 Alt 키를 나타내지만 이는 키의 X 서버 매핑을 기반으로 해석된다. Meta, Super 또는 Hyper 키에 해당하기도 한다.
  • GDK_SUPER_MASK: 2.10에서 소개되었고, Super 수식키를 명시적으로 나타내도록 해준다. 모든 시스템에서 이 수식키를 이용할 수 있는 것은 아니다!
  • GDK_HYPER_MASK: 2.10에서 소개되었고, Hyper 수식키를 명시적으로 나타내도록 해준다. 모든 시스템에서 이 수식키를 이용할 수 있는 것은 아니다!
  • GDK_META_MODIFIER: 2.10에서 소개되었고, Meta 수식키를 명시적으로 나타내도록 해준다. 모든 시스템에서 이 수식키를 이용할 수 있는 것은 아니다!


대부분의 경우 개발자는 GtkCellRendererAccel을 이용해 수식키 마스크(acel-mods)와 가속기 키 값(accel-key)을 트리 뷰 열의 두 가지 속성으로 설정하고자 할 것이다. 이런 경우 수식키 마스크는 G_TYPE_INT 타입이, 가속기 키 값은 G_TYPE_UNIT 타입이 될 것이다. 이 때문에 개발자는 수식키 마스크 열의 내용을 설정할 때 GdkModifierType 값이 확실히 gint로 설정되도록 확보해야 할 것이다.

store = gtk_list_store_new (COLUMNS, G_TYPE_STRING, G_TYPE_INT, G_TYPE_UINT);


GtkCellRendererAccel은 두 개의 시그널을 제공한다. 첫 번째 accel-cleared 시그널은 사용자가 현재 값을 제거하면 가속기를 리셋하도록 해준다. 가속기를 되돌릴만한 기본값이 없는 경우를 제외한 대부분의 사례에서는 이 시그널을 이용할 필요가 없을 것이다.


accel-edited는 이보다 더 중요한 시그널에 해당하는데, 개발자가 editable 프로퍼티를 TRUE로 설정하는 한 사용자가 변경한 내용을 키보드 가속기로 적용하도록 해준다. 콜백 함수는 가속기 키 코드, 마스크 및 하드웨어 키 코드를 비롯해 문제의 행에 대한 문자열 경로를 수신한다. 콜백 함수에서 변경 내용을 적용하려면 여느 편집 가능한 타입의 셀과 마찬가지로 gtk_list_store_set()을 이용한다.


자신의 이해도 시험하기

연습문제 8-1에서는 여러 타입의 셀 렌더러와 함께 GtkTreeView 위젯을 연습 삼아 사용할 기회를 제공할 것이다. GtkTreeView 위젯은 많은 애플리케이션에서 사용해야 할 것이기 때문에 매우 중요한 연습이 되겠다. 언제나처럼 연습문제가 끝나면 부록 F에서 해답을 확인할 수 있다.


연습문제 8-1. 파일 브라우저

지금쯤이면 Grocery List 애플리케이션을 충분히 학습했을테니 조금 다른 걸 시도해보자. 이번 연습문제에서는 GtkTreeView 위젯을 이용해 파일 브라우저를 생성하라. 파일 브라우저에 대해 GtkListStore를 이용하여 사용자가 파일 시스템을 훑어볼 수 있도록 하라.


파일 브라우저는 디렉터리와 파일을 구별하기 위해 이미지를 표시해야 한다. 이미지는 www.gtkbook.com에서 다운로드할 수 있는 소스 코드에서 찾을 수 있다. GLib 디렉터리 유틸리티 함수를 이용해 디렉터리 내용을 검색할 수도 있다. 디렉터리를 더블 클릭하면 해당 위치로 이동한다.


요약

이번 장에서는 GtkTreeView 위젯을 어떻게 사용하는지에 대해 학습하였다. 해당 위젯은 GtkListStore와 GtkTreeStore를 이용해 데이터의 리스트와 트리 리스트를 각각 표시하도록 해준다. 트리 뷰, 트리 모델, 열, 셀 렌더러 간 관계와 이러한 객체를 각각 어떻게 이용하는지도 학습하였다.


다음으로 트리 뷰 내에서 행을 가리키는 데 사용할 수 있는 객체의 타입들도 배웠다. 이러한 객체 타입으로는 트리 반복자, 경로, 행 참조가 있다. 각 객체 타입에는 장점과 단점이 있다. 트리 반복자는 모델과 직접 이용이 가능하지만 트리 모델이 변경되면 무효화된다. 트리 경로는 사람이 읽을 수 있는 문자열로 연관되어 이해가 쉽지만 트리 모델이 변경되면 동일한 행을 가리키지 않을 수도 있다. 마지막으로 트리 행 참조는 모델이 변경되더라도 행이 존재하는 한 유효하게 유지되기 때문에 유용하게 사용된다.


그 다음으로 단일 행이나 다중 행의 선택을 처리하는 방법을 학습하였다. 다중 행 선택의 경우 for-each 함수를 이용하거나, 선택된 행의 GList 리스트를 얻는 방법이 있다. 선택내용을 처리하는 데에는 GtkTreeView의 row-activated 시그널이 유용한데, 이는 더블 클릭을 처리하도록 해준다.


그에 이어 GtkCellRendererText의 edited 시그널을 이용해 편집 가능한 셀을 생성하는 방법을 알아보았는데, 이는 사용자가 셀의 내용을 편집하도록 GtkEntry를 표시한다. 셀 데이터 함수 또한 열로 연결이 가능하다. 이러한 셀 데이터 함수는 셀이 화면으로 렌더링되기 전에 개발자가 각 셀을 맞춤설정하도록 해준다.


마지막으로는 토글 버튼, pixbuf, 스핀 버튼, 콤보 박스, 진행 막대, 키보드 가속기 문자열을 표시하도록 해주는 다양한 셀 렌더러를 학습하였다. 동시에 GtkComboBox 위젯을 소개하였다.


축하한다! 이제 GTK+에서 제공하는 가장 까다로우면서도 다목적으로 사용되는 위젯에 익숙해졌다. 다음 장에서는 메뉴, 툴바, 팝업 메뉴에 대해 배워볼 것이다. 또 사용자 인터페이스(UI) 파일을 이용해 메뉴 생성을 자동화하는 방법도 알아보겠다.


Notes