# TreeView와 ListStore(JavaScript)

Taryn Fox
jewelfox@fursona.net 2012
각각의 항목 목록을 보여주는 위젯
조성호
shcho@gnome.org 2017-2019.

# TreeView와 ListStore

TreeView는 ListStore 또는 TreeStore의 내용을 보여주는 창입니다. ListStore는 스프레드 시트와 같습니다. 2차원 평면 자료구조이며 행과 열로 나뉩니다. 반면에 TreeStore는 트리로 할 수 있듯이 자료 묶음 방향을 가지로 쪼갤 수 있습니다. 이 예제에서는 TreeView로 가상의 이름과 전화번호가 들어간 ListStore의 내용을 보여주고, 누른 항목의 이름이 무엇인지 자세한 정보를 보여주도록 창 하단의 Label에 설정하겠습니다.

TreeView는 단일 위젯이 아니지만 여러 작은 부분을 담고 있습니다:

TreeViewColumn 위젯은 ListStore의 각(수직) 열 정보를 보여줍니다. 각각의 TreeViewWidget은 스크린샷에서 보시는 바와 같이 열 상단에 나타낼 제목을 지니고 있습니다.

CellRenderer 위젯은 각 TreeViewColumn에 "패킹" 처리하고, 각 "셀" 또는 ListStore의 항목을 표시하는 방식의 지시 사항을 담습니다. 여기서 사용하는 CellRendererText 그리고 그림("픽셀 버퍼")를 나타내는 CellRendererPixBuf 같은 다양한 형식이 있습니다.

마지막으로 ListStore에서 (가로 방향) 행을 가리키는 보이지 않는 커서로 위젯이 아닌 TreeIter 객체를 활용하겠습니다. 예를 들어 전화번호부에 이름을 누르면, 선택한 줄을 가리키는 TreeIter를 만드는데, TreeIter는 레이블에 더 많은 정보를 보여줄 항목이 무엇인지 ListStore에 지시할 때 활용합니다.

TreeView는 아마도 Gtk 위젯 중에 제일 복잡한 gtk 위젯이 아닐까 하는데, 위젯이 지닌 부분의 양과 함께 동작하는 방식 때문입니다. 동작 방식과 실험을 배울 시간을 확보하시든지, 하다가 문제 생기면 쉬운 부분 부터 먼저 진행하십시오.

# 가져올 라이브러리

#!/usr/bin/gjs

const GObject = imports.gi.GObject; const Gtk = imports.gi.Gtk; const Lang = imports.lang; const Pango = imports.gi.Pango;

이 프로그램을 실행할 때 가져올 라이브러리입니다. 시작 부분에 항상 gjs가 필요함을 알리는 줄을 작성해야 함을 기억하십시오.

# 프로그램 창 만들기

const TreeViewExample = new Lang.Class({ Name: 'TreeView Example with Simple ListStore',

// Create the application itself _init: function() { this.application = new Gtk.Application({ application_id: 'org.example.jstreeviewsimpleliststore' });

// Connect 'activate' and 'startup' signals to the callback functions this.application.connect('activate', Lang.bind(this, this._onActivate)); this.application.connect('startup', Lang.bind(this, this._onStartup)); },

// Callback function for 'activate' signal presents window when active _onActivate: function() { this._window.present(); },

// Callback function for 'startup' signal builds the UI _onStartup: function() { this._buildUI (); },

이 예제의 모든 코드는 TreeViewExample 클래스에 들어갑니다. 위 코드는 위젯과 창이 들어갈 Gtk.Application을 만듭니다.

// Build the application's UI _buildUI: function() {

// Create the application window this._window = new Gtk.ApplicationWindow({ application: this.application, window_position: Gtk.WindowPosition.CENTER, default_height: 250, default_width: 100, border_width: 20, title: "My Phone Book"});

_buildUI 함수는 프로그램 사용자 인터페이스를 만드는 모든 코드를 넣는 곳입니다. 첫 단계에서는 모든 위젯을 우겨넣을 새 Gtk.ApplicationWindow를 만듭니다.

# ListStore 만들기

// Create the underlying liststore for the phonebook this._listStore = new Gtk.ListStore (); this._listStore.set_column_types ([ GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_STRING]); 읽

우선 어떤 위젯도 다룰 수 있도록 ListStore를 만들겠습니다. 그 다음 set_column_types 메서드를 호출하여 GObject 데이터 형식의 배열에 전달하겠습니다(한 줄에 모든 형식을 넣을 수 있지만, 보기 쉽게 하려는 목적으로 쪼갰습니다).

여러분이 사용할 수 있는 GObject 데이터 형식은 다음과 같습니다:

GObject.TYPE_BOOLEAN -- 참/거짓

GObject.TYPE_FLOAT -- 소수(소수점 하나 들어감)

GObject.TYPE_STRING -- 문자, 숫자로 이루어진 문장

gtk.gdk.Pixbuf -- 그림

이 경우, 각 칸마다 문자열 값이 들어가는 4칸을 지닌 ListStore를 만듭니다.

GObject 형식을 사용할 수 있게 하려면, 우리가 예제에서 했던 것처럼 프로그램 코드 시작 부분에 const GObject = imports.gi.GObject; 를 넣으세요.

// Data to go in the phonebook this.phonebook = let phonebook = [{ name: "Jurg", surname: "Billeter", phone: "555-0123", description: "A friendly person."}, { name: "Johannes", surname: "Schmid", phone: "555-1234", description: "Easy phone number to remember."}, { name: "Julita", surname: "Inca", phone: "555-2345", description: "Another friendly person."}, { name: "Javier", surname: "Jardon", phone: "555-3456", description: "Bring fish for his penguins."}, { name: "Jason", surname: "Clinton", phone: "555-4567", description: "His cake's not a lie."}, { name: "Random J.", surname: "Hacker", phone: "555-5678", description: "Very random!"}];

ListStore에 정보를 넣었습니다. 전화번호부의 단일 항목에 해당하는 객체 배열입니다.

참고로 스크린샷의 TreeView는 실제로 "description" 속성의 데이터를 보여주지 않습니다. 대신 레이블 아래 여러분이 클릭해서 나타난 정보를 보여줍니다. 왜냐면 TreeView와 ListStore는 별개의 존재고, TreeView는 ListStore의 모든 부분을 보여줄 수 있으며, 무엇이 들어있는지 보여주는 방식이 다르기 때문입니다. 우리 예제의 레이블 내지는 두번째 TreeView 처럼 동일한 ListStore의 내용을 여러 위젯으로 보여줄 수 있습니다.

for (i = 0; i < phonebook.length; i++ ) { let contact = phonebook [i]; this._listStore.set (this._listStore.append(), [0, 1, 2, 3], [contact.name, contact.surname, contact.phone, contact.description]); }

for 루프로 전화번호부에서 가져온 문자열을 ListStore에 순서대로 넣습니다. 순서대로 ListStore의 set 메서드로 설정하고자 하는 열의 배열, 각 열에 넣어두고나 하는 데이터가 들어간 배열, 올바른 열을 가리키는 iter 값을 전달합니다.

ListStore의 append 메서드로 수평줄을 추가하고(아무것도 없는 상태에서 시작), 커서와 같은 해당 줄에 대한 TreeIter 포인터를 반환합니다. 따라서 ListStore에 this._listStore.append() 함수 값을 속성으로 전달하여 새 줄을 만들고 set 메서드를 통해 방금 만든 새 줄에 통시에 데이터를 넣도록 하겠습니다.

# TreeView 만들기

// Create the treeview this._treeView = new Gtk.TreeView ({ expand: true, model: this._listStore });

여기서는 기본적인 TreeView 위젯을 만들어, 가로 세로 방향으로 필요한 만큼 충분한 공간을 늘릴수 있게 하겠습니다. 우리가 만든 ListStore를 "모델"로 사용하거나, 어딘가로부터 가져온 내용을 활용하게 하겠습니다.

// Create the columns for the address book let firstName = new Gtk.TreeViewColumn ({ title: "First Name" }); let lastName = new Gtk.TreeViewColumn ({ title: "Last Name" }); let phone = new Gtk.TreeViewColumn ({ title: "Phone Number" });

이제 TreeView에서 볼 각각의 TreeViewColumn을 만들겠습니다. 각 TreeViewColumn의 제목은, 스크린샷에서 보시는 대로 상단에 올라갑니다.

// Create a cell renderer for when bold text is needed let bold = new Gtk.CellRendererText ({ weight: Pango.Weight.BOLD });

// Create a cell renderer for normal text let normal = new Gtk.CellRendererText ();

// Pack the cell renderers into the columns firstName.pack_start (bold, true); lastName.pack_start (normal, true); phone.pack_start (normal, true);

여기서 ListStore의 텍스트를 나타내는데 활용할 CellRenderer를 만들었고 TreeViewColumns에 감쌌습니다. 각 CellRendererText는 칸에 들어간 모든 항목에 사용합니다. 굵은 글씨에는 더 굵은 텍스트를 사용하지만, 일반적인 CellRendererText는 그냥 텍스트를 만듭니다. 일단 이름 열에 넣고, 다른 두 부분은 일반 CellRendererText로 넣으라고 하겠습니다. pack_start 메서드 두번째 매개변수 값으로 사용하는 "true" 값은 가능할 경우 작은 상태로 유지하지 말고 셀 크기를 확장하라고 지시합니다.

여러분이 활용할 수 있는 다른 텍스트 속성 목록 입니다. 팡고 상수를 활용하려면, 우리가 했던 바와 같이 const Pango = imports.gi.Pango; 줄을 코드 앞부분에 넣었는지 확인하십시오.

firstName.add_attribute (bold, "text", 0); lastName.add_attribute (normal, "text", 1); phone.add_attribute (normal, "text", 2);

// Insert the columns into the treeview this._treeView.insert_column (firstName, 0); this._treeView.insert_column (lastName, 1); this._treeView.insert_column (phone, 2);

이제 CellRenderer를 TreeViewColumns에 넣었습니다. add_attribute 메서드를 활용하여 활용하기로 설정한 TreeView 모델에서 텍스트를 가져와서 각 칸에 뿌리도록 하겠습니다. 이 경우 전화번호부가 들어간 ListStore가 되겠습니다.

첫번째 매개변수는 우리가 렌더러에 데이터를 가져다 넣을때 활용할 CellRenderer입니다.

두번째 매개변수는 가져다젛을 정보의 종류입니다. 여기서는 텍스트를 렌더링하라고 알려줍니다.

세번째 매개변수는 우리가 가져올 정보가 들어간 ListStore의 열 위치를 말합니다.

설정하고 나면 TreeView의 insert_column 메서드를 활용하여 TreeViewColumns 네 순서대로 넣겠습니다. 이제 TreeView 구현이 끝났습니다.

보통 TreeView를 초기화하려고 루프를 사용하고 싶을지도 모릅니다만, 이 예제에서는 이해를 쉽게 하기 위한 단계별로 하나하나 설명하겠습니다.

# 나머지 사용자 인터페이스 구성

// Create the label that shows details for the name you select this._label = new Gtk.Label ({ label: "" });

// Get which item is selected this.selection = this._treeView.get_selection();

// When something new is selected, call _on_changed this.selection.connect ('changed', Lang.bind (this, this._onSelectionChanged));

TreeView의 get_selection 메서드는 TreeSelection 객체를 반환합니다. TreeSelection은 행 일부를 가리키는 근본 커서로서 TreeIter와 유사하지만, TreeSelection에는 선택한 부분을 눈에 띄게 강조하는 차이점을 안고 있습니다.

TreeSelection을 TreeView에서 가져오고 난 후, TreeSelection에 가리키는 행이 어디인지 요청할 수 있습니다. changed 시그널을 우리가 작성한 _onSelectionChanged 함수에 연결하면 됩니다. 이 함수는 우리가 만든 Label의 텍스트를 바꿉니다.

// Create a grid to organize everything in this._grid = new Gtk.Grid;

// Attach the treeview and label to the grid this._grid.attach (this._treeView, 0, 0, 1, 1); this._grid.attach (this._label, 0, 1, 1, 1);

// Add the grid to the window this._window.add (this._grid);

// Show the window and all child widgets this._window.show_all(); },

위 과정을 끝내고 나면, 모든 위젯을 넣을 Grid를 만들고, Grid를 창에 추가하여 창과 창에 있는 내용을 표시하도록 합니다.

# 바뀐 선택을 처리하는 함수

_onSelectionChanged: function () {

// Grab a treeiter pointing to the current selection let [ isSelected, model, iter ] = this.selection.get_selected();

// Set the label to read off the values stored in the current selection this._label.set_label ("\n" + this._listStore.get_value (iter, 0) + " " + this._listStore.get_value (iter, 1) + " " + this._listStore.get_value (iter, 2) + "\n" + this._listStore.get_value (iter, 3));

}

});

let 문 코드 줄은 조금 복잡하긴 하지만, 그래도 TreeSelection에서 동일한 줄을 가리키는 TreeIter를 가져오는 최상의 수단입니다. 다른 수많은 객체 참조를 들고 있긴 하지만, iter만 필요합니다.

이 과정이 끝나면 레이블의 set_label 함수를 호출하고 ListStore의 get_value 함수를 활용하여 짦은 시간 내에 원하는 데이터를 채워넣겠습니다. 매개 변수는 우리가 가져온 줄 위치와 열을 가리키는 TreeIter입니다.

TreeView의 일부가 아닌 "hidden"을 네 열의 데이터와 가져오려합니다. 여기서 TreeView에 맞추긴 너무 긴 문자열을 Label에 표시하여 굳이 살펴볼 필요가 없게 하겠습니다.

// Run the application let app = new TreeViewExample (); app.application.run (ARGV);

마지막으로 TreeViewExample 클래스의 새 인스턴스를 만들고 프로그램 실행을 설정하겠습니다.

# 완전한 코드 예제

#!/usr/bin/gjs

imports.gi.versions.Gtk = '3.0';

const GObject = imports.gi.GObject; const Gtk = imports.gi.Gtk; const Pango = imports.gi.Pango;

class TreeViewExample { // Create the application itself constructor() { this.application = new Gtk.Application({ application_id: 'org.example.jstreeviewsimpleliststore' });

// Connect 'activate' and 'startup' signals to the callback functions this.application.connect('activate', this._onActivate.bind(this)); this.application.connect('startup', this._onStartup.bind(this)); }

// Callback function for 'activate' signal presents window when active _onActivate() { this._window.present(); }

// Callback function for 'startup' signal builds the UI _onStartup() { this._buildUI(); }

// Build the application's UI _buildUI() { // Create the application window this._window = new Gtk.ApplicationWindow({ application: this.application, window_position: Gtk.WindowPosition.CENTER, default_height: 250, default_width: 100, border_width: 20, title: "My Phone Book"});

// Create the underlying liststore for the phonebook this._listStore = new Gtk.ListStore (); this._listStore.set_column_types ([ GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_STRING]);

// Data to go in the phonebook let phonebook = [{ name: "Jurg", surname: "Billeter", phone: "555-0123", description: "A friendly person."}, { name: "Johannes", surname: "Schmid", phone: "555-1234", description: "Easy phone number to remember."}, { name: "Julita", surname: "Inca", phone: "555-2345", description: "Another friendly person."}, { name: "Javier", surname: "Jardon", phone: "555-3456", description: "Bring fish for his penguins."}, { name: "Jason", surname: "Clinton", phone: "555-4567", description: "His cake's not a lie."}, { name: "Random J.", surname: "Hacker", phone: "555-5678", description: "Very random!"}];

// Put the data in the phonebook for (let i = 0; i < phonebook.length; i++ ) { let contact = phonebook [i]; this._listStore.set (this._listStore.append(), [0, 1, 2, 3], [contact.name, contact.surname, contact.phone, contact.description]); }

// Create the treeview this._treeView = new Gtk.TreeView ({ expand: true, model: this._listStore });

// Create the columns for the address book let firstName = new Gtk.TreeViewColumn ({ title: "First Name" }); let lastName = new Gtk.TreeViewColumn ({ title: "Last Name" }); let phone = new Gtk.TreeViewColumn ({ title: "Phone Number" });

// Create a cell renderer for when bold text is needed let bold = new Gtk.CellRendererText ({ weight: Pango.Weight.BOLD });

// Create a cell renderer for normal text let normal = new Gtk.CellRendererText ();

// Pack the cell renderers into the columns firstName.pack_start (bold, true); lastName.pack_start (normal, true); phone.pack_start (normal, true);

// Set each column to pull text from the TreeView's model firstName.add_attribute (bold, "text", 0); lastName.add_attribute (normal, "text", 1); phone.add_attribute (normal, "text", 2);

// Insert the columns into the treeview this._treeView.insert_column (firstName, 0); this._treeView.insert_column (lastName, 1); this._treeView.insert_column (phone, 2);

// Create the label that shows details for the name you select this._label = new Gtk.Label ({ label: "" });

// Get which item is selected this.selection = this._treeView.get_selection();

// When something new is selected, call _on_changed this.selection.connect ('changed', this._onSelectionChanged.bind(this));

// Create a grid to organize everything in this._grid = new Gtk.Grid;

// Attach the treeview and label to the grid this._grid.attach (this._treeView, 0, 0, 1, 1); this._grid.attach (this._label, 0, 1, 1, 1);

// Add the grid to the window this._window.add (this._grid);

// Show the window and all child widgets this._window.show_all(); }

_onSelectionChanged() { // Grab a treeiter pointing to the current selection let [ isSelected, model, iter ] = this.selection.get_selected();

// Set the label to read off the values stored in the current selection this._label.set_label ("\n" + this._listStore.get_value (iter, 0) + " " + this._listStore.get_value (iter, 1) + " " + this._listStore.get_value (iter, 2) + "\n" + this._listStore.get_value (iter, 3) ); } };

// Run the application let app = new TreeViewExample (); app.application.run (ARGV);

# 자세한 문서

이 예제는 다음 참고자료가 필요합니다:

Gtk.Application

Gtk.ApplicationWindow

Gtk.CellRendererText

Gtk.ListStore

Gtk.TreeIter

Gtk.TreeSelection

Gtk.TreeView

Gtk.TreeViewColumn