GNOME3ApplicationDevelopmentBeginnersGuide:Chapter 05
- 제 5 장 그래픽 사용자 인터페이스 애플리케이션 빌드하기
그래픽 사용자 인터페이스 애플리케이션 빌드하기
GTK+는 프로젝트가 시작될 때부터 사실상 GNOME을 위한 그래픽 사용자 인터페이스(GUI) 툴킷으로 사용되었다. 최근에는 Clutter가 대안책으로 통합되면서 OpenGL가 제공하는 유체 애니메이션 사용자 인터페이스를 제공하고 있다. 이 둘은 GNOME용 GUI 애플리케이션을 개발 시 선택할 수 있는 훌륭한 기본 툴킷 집합이다.
이번 장에서는 GTK+와 Clutter를 이용해 애플리케이션을 생성하는 방법을 학습할 것이다. 구체적으로 학습하게 될 내용은 다음과 같다.
- 기본 GTK+ 애플리케이션 생성하기
- 일반 GTK+를 이용해 프로그래밍하기
- Clutter를 이용해 프로그래밍하기
시작하기 전에
Clutter로 애플리케이션을 개발하기 위해서는 제 1장, GNOME3와 SDK 설치하기에서 언급했듯이 OpenGL이 가능한 환경이 작동해야 한다. 환경이 지원되지 않으면 프로그램은 시작되지 않을 것이다. 하지만 GTK+ 애플리케이션 환경은 어떠한 하드웨어 지원도 요구하지 않는다.
기본 GTK+ 애플리케이션 생성하기
GTK+의 기본 위젯인 라벨과 버튼의 사용부터 시작해보자. 제 2장, 무기 준비하기에서 목업(mockup)을 살펴본 적이 있는데, 이번에도 애플리케이션을 생성하기 위한 계획으로 목업을 이용하고자 한다. 그렇지만 계획을 좀 더 구체적으로 만들기 위해 목업에 기능을 추가할 것이다. 아래 목업과 같은 애플리케이션을 생성하길 원한다고 가정해보자.
목업의 각 이미지는 애플리케이션의 특정 상태를 표시한다. 한 이미지에서 다른 이미지로 따라가면 전체적인 시퀀스 흐름을 만드는데, 이를 상호작용 흐름(interaction flow)이라 부른다. 구체적으로 말하자면 이것은 사용자의 상호작용 직후에 애플리케이션의 반응을 표시한다. 사용자 상호작용은 그래픽 아이콘으로 표시되는데, 이는 사용자가 상호작용하는 애플리케이션 화면에서 항목의 젤 위에 그려진다. 이러한 목업에서는 클릭 상호작용만 가능한데, 이는 항목에 마우스 클릭이 실행되었음을 나타내기 위해 점으로 된 원으로 표시된다.
목업을 좀 더 가까이 살펴보자. 첫 번째 애플리케이션 상태는 이미지 번호 1로 표시된다. 이는 중앙으로 정렬된 라벨과 세 개의 버튼을 표시하는데, 가운데 버튼은 누름 상태로 되어 있다. 이미지 번호 2는 사용자가 첫 번째 버튼을 클릭함을 보여준다. 이미지 번호 3은 애플리케이션의 반응을 표시한다. 이미지가 왼쪽으로 이동하였고, 버튼 상태는 누름 상태로 변경되었음을 확인할 수 있다. 또 중간 버튼의 상태는 일반 상태로 설정되었음을 확인할 수 있다. 다음 이미지를 따라 끝까지 가면 애플리케이션에서 가능한 상호작용을 모두 갖게 된다.
여기서 어떤 위젯을 사용해야 할 것인지 계획할 수 있다. 창의 경우 간단한 GtkWindow를 이용할 수 있겠다. 라벨은 애플리케이션 중앙에 GtkLabel을 이용할 수 있다. 버튼에 일반 GtkButton을 사용할 경우, 하나의 버튼만 눌러도 모든 버튼을 리셋해야 한다. 따라서 GtkRadioButton이라는 특수 버튼을 이용해 우리가 누르는 버튼을 제외하고 모든 버튼을 비활성(inactive) 상태로 설정하는 기능을 구현하고자 한다. 어떤 위젯을 사용할 것인지 알고 있으니 구현을 시작할 수 있겠다.
실행하기 - 목업 구현하기
제 2장, 무기 준비하기에서 GUI 애플리케이션을 간략하게 만들어봤기 때문에 이제는 목업에 따라 라벨과 버튼을 이용하는 실험을 하기 위해 JavaScript에서 애플리케이션을 생성할 것이다.
- Glade를 독립형 프로그램으로 실행하고 gtk-basic-widgets.uni라는 새로운 파일을 생성하여 gtk-basic-widgets라는 전용 디렉터리에 넣어라.
- Palette dock에서 Container 섹션 안에 Box 객체를 찾아라. 객체를 클릭하고 오른쪽에 빈 창을 클릭하라. Number of items 값을 질문하면 2라고 답한다. -와 + 버튼을 사용하거나 숫자 2를 넣어도 좋다.
- 이제 창의 박스가 두 개의 박스로 나뉘었음을 확인할 수 있을 것인데, 이를 상단 박스와 하단 박스라고 부르자.
- 다시 Container 섹션 안에 Box 객체를 클릭하고 하단 박스를 클릭한다. 하단 박스는 세 개의 박스로 나뉠 것이다.
- 두 번째 박스 그룹은 자동으로 box2 로 명명된다. box2 의 General 탭에서 Orientation 값을 확인하라. 이 값을 Horizontal 로 변경하라.
- General 탭에서 Homogeneous 값을 Yes 로 설정하라.
- 하단 박스가 수평으로 세 개의 박스로 나뉘었을 것이다. 이러한 박스를 왼쪽, 중앙, 오른쪽 박스라고 기억하자.
- Palette dock의 Control and Display 섹션에서 label 을 찾아라. label 을 클릭한 후 상단 박스를 클릭하라. 이제 라벨이 상단 박스에 위치할 것이다.
- Widget dock에서 label1 을 찾아 Packing 탭을 클릭하라. Expand 값을 클릭하여 Yes 로 변경하라.
- 이제 라벨이 상단 박스로 영역을 확장한다.
- Palette dock의 Control and Display 섹션에서 Toggle Button 을 찾아라. 이를 클릭한 후 왼쪽 박스를 클릭하라. Toggle Button 을 계속해서 클릭한 후 중앙 박스를 클릭하고, 마지막으로 오른쪽 박스를 클릭하라.
- 중간 버튼과 오른쪽 버튼의 경우 버튼 그룹을 명시하여 선택 가능한 상태가 되도록 만들어야 한다. 이 버튼들의 General 탭에서 Group 옵션을 찾아라. 타원형 버튼(세 개의 점으로 표시)을 클릭한 후 radiobutton1 을 선택하라.
- 이제 모든 버튼이 하단 박스를 채운다.
- 중간 버튼을 클릭하고 Active 값을 (General 탭에서 찾을 수 있다) Yes 로 설정하라.
- 라벨을 클릭하고 General 탭을 연 후 Label 의 값을 in the center 로 설정하라.
- 왼쪽 버튼을 클릭하고 Label on optional image 옵션을 클릭한 후 Label 을 Go Left 로 채워라. 중간 버튼과 오른쪽 버튼에도 동일한 액션을 반복하고, 라벨을 각각 Go Center 와 Go Right 로 설정하라.
- 각 버튼에서 General 탭으로 가서 Draw indicator 를 No 로, Horizontal alignment for child 를 0.5 로 설정하여 모든 버튼에 반복한다.
- Gtk-basic-widgets.js 라는 새로운 스크립트를 생성하고 아래와 같은 코드로 채워라.
#!/usr/bin/env seed Gtk= imports.gi.Gtk; GObject = imports.gi.GObject; Main = new GType({ parent: GObject.Object.type, name: "Main", init: function(self) { this.go_left = function(object) { if (object.active) { self.label.set_label("going left"); self.label.set_alignment(0, 0.5); } } this.go_center = function(object) { if (object.active) { self.label.set_label("in the center"); self.label.set_alignment(0.5, 0.5); } } this.go_right = function(object) { if (object.active) { self.label.set_label("going right"); self.label.set_alignment(1, 0.5); } } var ui = new Gtk.Builder() this.ui = ui; ui.add_from_file("gtk-basic-widgets.ui"); var window = ui.get_object("window1"); window.resize(300, 400); window.show_all(); window.signal.destroy.connect(Gtk.main_quit); this.label = ui.get_object("label1"); ui.get_object("radiobutton1").signal.toggled.connect(this.go_left); ui.get_object("radiobutton2").signal.toggled.connect(this.go_center); ui.get_object("radiobutton2").set_active(true); ui.get_object("radiobutton3").signal.toggled.connect(this.go_right); } }); Gtk.init(Seed.argv); var main = new Main(); Gtk.main();
- 애플리케이션을 실행하라. 버튼을 눌러보고 목업이 완성되었는지 확인하라.
무슨 일이 일어났는가?
제 2장, 무기 준비하기에서 살펴보았듯이 창은 하나의 위젯만 취할 수 있기 때문에 위젯들(버튼과 라벨)을 창으로 직접 추가할 수는 없다. 따라서 하나 이상의 위젯을 보유할 수 있는 컨테이너 위젯이 필요하다. 바람직한 모습이 되도록 먼저 창을 두 가지 부분, 상단과 하단으로 나눈다. 이후 또 다른 컨테이너를 하단 부분에 넣는다. 이러한 컨테이너 집합을 이용해 위젯을 내부로 넣을 수 있다.
이 예제에서 우리는 GtkBox(Glade에서 Box 로 표시)를 컨테이너로 사용하고, 부분을 어떻게 나눌 것인지 나타내기 위해 방향을 설정한다. GtkBox는 시각적 부분을 갖고 있지 않다. 내부에 위치한 위젯들이 그곳으로 할당된 전체 영역을 덮도록 해준다. 위젯을 Box 내부에 패킹하면 위젯은 선호되는 높이에 관한 힌트를 Box로 제공하고, Box는 그 높이에 따라 영역을 계산 및 할당하여 위젯이 스스로 영역에 그려지도록 허용한다. Box는 우리가 명시한 방향에 따라 그에 추가된 위젯들을 자동으로 배열한다. 따라서 특정 좌표로 위젯을 넣지 않고 Box가 위젯을 배열하는 방식에 의존한다.
첫 번째 박스는 수직 박스로, 하단 부분은 일정한 크기로 유지하되 상단 부분은 창 크기에 따라 크기를 조정하길 원한다. 이를 구현하려면 구체적으로 상단 박스 안에 위치한 label1 위젯을 만들어 위젯이 패킹될 때 Expand 패킹 옵션을 Yes 로 설정하여 확장하면 부모의 크기가 증가할 때 라벨이 추가 공간을 제공할 것이다. 기본적으로 Yes 로 설정되는 Fill 옵션과 함께 Box는 라벨의 실제 높이를 제공하는 대신 최대 높이를 제공할 것이다.
여기서 Box는 상단과 하단 부분 안의 모든 위젯에 선호되는 크기를 계산하고 창의 크기에 따라 이용할 수 있는 나머지 크기를 알아내는 일을 실행한다. 이후 나머지 크기를 라벨의 원래 크기로 추가하여 상단 부분 안에 있는 label1 위젯의 크기를 최대화한다.
하단 부분을 직접 나눌 수는 없는 것은 Box 객체가 하나의 방향만 가질 수 있기 때문이다. 직접 나누기 위해서는 하단 부분 안에 새로운 수평적 Box 위젯을 추가해야 한다. 이러면 상황은 약간 달라진다. 여기서 Box 방향은 수평적이고 세 개의 부분으로 균등하게 나누길 원하므로 Homogeneous 옵션을 활성화할 필요가 있다.
토글 버튼에서 우리는 radiobutton1을 그룹 리더로 사용한다. 이것을 이용할 경우 하나의 버튼을 누르면 나머지 버튼들은 비활성(inactive)가 된다. Draw indicator 옵션을 비활성화하고 버튼 내 모든 라벨을 중앙으로 정렬함으로써 일반 버튼처럼 보이도록 만들기도 해야 한다.
이 예제에서 우리는 상속구조에 다수의 위젯을 넣었는데, 이는 어떤 위젯들에겐 부모와 자식이 있다는 뜻이다. 가령 창에는 자체의 자식들과 창에 부착된 모든 위젯의 자식들이 포함된다. 수직적 Box 위젯은 라벨의 자식들, 수평적 Box 위젯과 그의 자식들을 갖게 된다. 수평적 Box 위젯은 모든 버튼을 그 자식으로 갖는 식이다. 상속구조를 이해하는 것은 애플리케이션의 구조체를 이해하는 데에 매우 중요하다. 상속구조는 Glade에서 Inspector dock에서 확인할 수 있다.
Gtk.init(Seed.argv);
var main = new Main();
Gtk.main();
여기에 GTK+의 초기화 코드가 있다. Gtk.init는 그래픽 환경을 준비하여 애플리케이션이 GUI 애플리케이션으로서 실행되도록 준비한다. Seed.argv는 애플리케이션으로 전달되는 인자의 리스트를 제공한다. 이번 예제에서는 빈 리스트가 되겠다. 그리고 Main 클래스를 초기화한다. 이후 Gtk.main을 호출함으로써 GTK+ 메인 루프를 입력한다.
이 코드 부분에서 Gtk.Builder 객체를 이용해 gtk-basic-widgets.ui 파일을 로딩한다.
this.ui = new Gtk.Builder()
this.ui.add_from_file("gtk-basic-widgets.ui");
경로가 올바른지 확인하라.
창의 이름, 즉 window1을 검색하여 창의 참조를 얻는다.
var window = this.ui.get_object("window1");
이름을 변경하면 이 코드에서도 마찬가지로 변경해야 한다.
이름만 올바르다면 Glade에서 사용하는 위젯은 무엇이든 get_object 함수를 이용해 찾을 수 있다.
이제 창의 크기를 조정하고 창의 내용을 모두 화면에 표시한다.
window.resize(300, 400);
window.show_all();
이 부분은 창이 닫힐 때마다 애플리케이션을 종료할 것이다.
window.signal.destroy.connect(Gtk.main_quit);
이 코드가 없이는 창이 닫혀도 애플리케이션이 계속 실행될 것이다. 애플리케이션이 아직도 실행 중인지 확인하는 방법으로, terminal에 ps 명령을 실행시키는 방법이 있다.
아래 코드에서는 모든 버튼의 토글 시그널(toggled signal)을 각각의 핸들러로 연결한다. 토글 시그널은 상태가 활성화에서 비활성 상태로 혹은 그 반대로 변경될 때마다 발생한다.
this.label = ui.get_object("label1");
ui.get_object("radiobutton1").signal.toggled.connect(this.go_left);
ui.get_object("radiobutton2").signal.toggled.connect(this.go_center);
ui.get_object("radiobutton2").set_active(true);
ui.get_object("radiobutton3").signal.toggled.connect(this.go_right);
radiobutton1 버튼이 토글되면 시그널 핸들러 go_left 함수가 호출된다. radiobutton2의 경우 go_center가 호출되고, radiobutton3이 토글되면 go_right가 호출된다. radiobutton2의 경우 활성화(active)로 초기화해야만 목업의 초기 상태와 일치한다.
go_left 함수에서 라벨을 going left 로 설정하고, set_alignment 함수의 첫 번째 변수를 0으로 설정함으로써 라벨을 왼쪽으로 정렬한다. 두 번째 변수는 수직 정렬용이다.
this.go_left = function(object) {
if (object.active) {
self.label.set_label("going left");
self.label.set_alignment(0, 0.5);
}
}
이는 항상 0.5로 설정된다. 정렬값 범위는 0(왼쪽)부터 1(오른쪽)까지다. 수직 정렬값 또한 0(상단)부터 1(하단)까지다. Glade에서 모든 버튼에 Horizontal alignment for child 를 0.5로 설정하면 버튼 안의 라벨이 중앙 정렬로 설정된다는 뜻임을 기억하라.
우리는 버튼이 활성화되었을 때만 이러한 설정(settings)을 설정하길 원한다. 따라서 if (object.active)를 이용해 함수를 보호해야만 라벨과 정렬 설정이 두 번 호출되는 것을 피할 수 있다. 이 예제에서는 가드(guard)를 갖고 있지 않더라도 그다지 차이가 없다. 하지만 실제 라이브 애플리케이션에서는 중복 호출을 피하기 위해 종종 가드가 필요한데, 중복 호출은 혼동을 야기하기도 하고 핸들러 내에 복잡한 계산이 있어 성능을 저하시키기도 하기 때문이다.
go_center와 go_right 함수에도 비슷한 일이 발생한다. 하지만 이 함수에서는 0.5 를 인자로 하여 중앙 정렬로, 1을 인자로 하여 오른쪽 정렬로 설정한다.
팝 퀴즈
Q1. 수평 정렬 값이 0.3일 경우 위젯의 시각적인 위치는 어디인가?
- 중앙
- 약간 오른쪽
- 약간 왼쪽
Q2. 수직 정렬 값이 0.3일 경우 위젯의 시각적인 위치는 어디인가?
- 중앙
- 약간 오른쪽
- 약간 왼쪽
시도해보기 - Vala 버전 생성하기
제 2장 무기 준비하기에서 사용한 예제를 이용해 Vala 버전으로 간단한 코드를 이식해볼 수 있겠다. 이러한 이식은 간단하고 수월해야 한다.
실행하기 - 아이콘을 버튼에 추가하기
이제 아이콘을 버튼으로 추가하길 원한다고 가정해보자. 아이콘은 주로 버튼에 의도된 기능을 설명하기 위해 버튼으로 추가된다. 버튼의 라벨이 짧거나 모호할 경우 더 중요해질 것이다. 아이콘을 버튼으로 추가하기 위해서는 아래 단계를 따라 해보자.
- gtk-basic-widgets.ui에 있을 때 왼쪽 버튼을 클릭하라.
- General 탭을 찾고, Label with option image 옵션 아래의 image widget 옵션을 찾아라.
- 타원형 버튼을 클릭하면 대화상자가 나타날 것이다.
- New 버튼을 클릭하면 이미지 위젯이 생성될 것이다.
- 이제 위젯 리스트에서 위젯을 찾아라. 처음에 image1로 명명될 것이다.
- 이미지를 클릭하고 General 탭을 찾아 Edit image 아래의 Stock ID 를 찾아라. 이를 클릭하고 Left 아이콘을 찾아라. 아이콘명은 gtk-justify-left다.
- 위의 과정을 반복하되 중간 버튼과 오른쪽 버튼에 Center (gtk-justify-center) 와 Right(gtk-justify-right) 아이콘을 사용하라.
- 코드에서 생성자 내에 아래의 행을 추가하라.
var s = Gtk.Settings.get_default(); s.gtk_button_images = true;
- 이를 실행하라.
무슨 일이 일어났는가?
애플리케이션의 위젯 상속구조 밖에서 이미지가 생성되었다. 즉, 부모 위젯이나 자식 위젯이 없다는 의미다. 그저 코드 "어딘가에" 생성되었을 뿐이다. 이미지는 버튼으로 묶음으로써 사용한다. 이미지는 기본적으로 표시되지 않을 것이다.
var s = Gtk.Settings.get_default();
s.gtk_button_images = true;
위의 코드를 이용하면 GTK+가 이미지를 표시한다.
시도해보기 - 아이콘 위치조정(placement)
자, 이제 Widgets dock의 General 탭에서 이용 가능한 프로퍼티를 살펴보라. 아이콘의 위치를 조정하는 데에 사용할 수 있는 Image position 프로퍼티가 보일 것이다. 이 프로퍼티를 이용하면 오른쪽 정렬 아이콘을 버튼의 오른쪽에, 중앙 정렬 아이콘을 버튼의 중앙에 위치하도록 설정할 수 있다. 프로퍼티의 스위치를 활성화시키면 될 일이다!
GtkBuilder 없이 코드 이식하기
지금까지는 Glade를 이용해 UI를 디자인하고 런타임 시 디자인을 로딩하기 위해서는 GtkBuilder를 사용해왔다. 본 서적의 나머지 부분에서는 Glade를 사용할 것이지만 현재로선 Glade를 사용할 경우 분명하지 않은 GTK+의 기본 내용을 이해하기 위해 저수준에서 GTK+ 프로그래밍을 실행하도록 하겠다. 프로그램이 갈수록 커지면서 언젠가 성능 문제, 레이아웃 문제 등에 직면하게 될 것이기 때문에 이러한 지식은 더욱 더 중요해진다.
실행하기 - raw GTK+를 이용해 프로그래밍하기
목업과 상호작용 흐름을 구현하기 위해 raw GTK+ 프로그래밍을 사용하는 JavaScript 코드를 실행할 것이다.
- gtk-basic-widgets-sans-glade.js 라는 JavaScript 파일을 그 이름에서 .js 확장자만 제외한 디렉터리에서 생성하라.
- 스크립트에 아래 코드를 사용하라.
#!/usr/bin/env seed Gtk = imports.gi.Gtk; GObject = imports.gi.GObject; Main = new GType({ parent: GObject.Object.type, name: "Main", init: function(self) { this.go_left = function(object) { if (object.active) { self.label.set_label("going left"); self.label.set_alignment(0, 0.5); } } this.go_center = function(object) { if (object.active) { self.label.set_label("in the center"); self.label.set_alignment(0.5, 0.5); } } this.go_right = function(object) { if (object.active) { self.label.set_label("going right"); self.label.set_alignment(1, 0.5); } } var window = new Gtk.Window(); window.signal.destroy.connect(Gtk.main_quit); var topBottomBox = new Gtk.Box(); topBottomBox.orientation = Gtk.Orientation.VERTICAL; topBottomBox.set_homogeneous(false); window.add(topBottomBox); var label = new Gtk.Label(); this.label = label; label.set_text("in the center"); topBottomBox.pack_start(label, true, true, 0); var buttonBox = new Gtk.Box(); buttonBox.set_homogeneous(true); topBottomBox.pack_start(buttonBox, false, false, 0); var leftButton = new Gtk.RadioButton.with_label(null, "Go left"); var centerButton = new Gtk.RadioButton.with_label_from_widget(leftButton, "Go center"); var rightButton = new Gtk.RadioButton.with_label_from_widget(leftButton, "Go right"); leftButton.signal.clicked.connect(this.go_left); centerButton.signal.clicked.connect(this.go_center); rightButton.signal.clicked.connect(this.go_right); leftButton.draw_indicator = centerButton.draw_indicator = rightButton.draw_indicator = false; centerButton.active = true; leftButton.xalign = 0.5; centerButton.xalign = 0.5; rightButton.xalign = 0.5; buttonBox.pack_start(leftButton, false, true, 0); buttonBox.pack_start(centerButton, false, true, 0); buttonBox.pack_start(rightButton, false, true, 0); window.show_all(); window.resize(300, 400); } }); Gtk.init(Seed.argv); var main = new Main(); Gtk.main();
- 위를 실행하라. Glade와 GtkBuilder를 사용하는 버전과 시각적으로 동일할 것이다.
무슨 일이 일어났는가?
앞의 실험과 비슷한 코드 부분에 대해서는 논하지 않고 수동 위젯의 선언을 좀 더 자세히 살펴보겠다.
먼저 GtkWindow를 생성하고 destroy 시그널을 연결하여 창이 닫힐 때마다 애플리케이션이 바람직하게 종료되도록 할 것이다.
var window = new Gtk.Window();
window.signal.destroy.connect(Gtk.main_quit);
이 부분에서 창을 상단 부분과 하단 부분으로 나누는 박스를 준비한다. 이 때 박스는 수직적이고 균일하지 않은(non-homogeneous) 박스임을 명시적으로 나타낸다. 이후 자식이 하나인 컨테이너 위젯, 즉 창으로 박스를 추가한다.
var topBottomBox = new Gtk.Box();
topBottomBox.orientation = Gtk.Orientation.VERTICAL;
topBottomBox.set_homogeneous(false);
window.add(topBottomBox);
다음으로 라벨을 준비하여 상단 박스로 패킹한다. Glade에서는 박스에 위치시킬 항목의 개수를 명시하는 반면 수동 GTK+ 프로그래밍에서는 항목의 개수를 미리 선언해야 하는 가능성 없이 항목을 하나씩 박스로 패킹하면 된다. Glade는 항목을 놓을 수 있는 플레이스홀더를 준비하기 위한 용도로 숫자를 필요로 한다.
var label = new Gtk.Label();
this.label = label;
label.set_text("in the center");
topBottomBox.pack_start(label, true, true, 0);
이 코드에서는 true로 설정된 Fill과 Expand 프로퍼티의 값을 이용해 label 변수를 패킹한다. 0 값은 패킹 중에 어떠한 패딩도 추가해선 안 됨을 의미한다.
이후 버튼을 위해 박스를 준비시킨다. 방향은 기본적으로 수평으로 설정되어 있기 때문에 별도로 손대지 않고 homogeneous 프로퍼티만 설정한다. 그리고 Fille과 Expand 값을 설정하지 않고 패킹하면 된다.
var buttonBox = new Gtk.Box();
buttonBox.set_homogeneous(true);
topBottomBox.pack_start(buttonBox, false, false, 0);
여기서 첫 번째 버튼을 생성한다.
var leftButton = new Gtk.RadioButton.with_label(null, "Go left");
다음으로 leftButton을 그룹 리더로 하여 두 개의 버튼이 추가된다.
var centerButton = new Gtk.RadioButton.with_label_from_widget(leftButton, "Go center");
var rightButton = new Gtk.RadioButton.with_label_from_widget(leftButton, "Go right");
이 부분은 아래 버튼의 클릭된 시그널을 연결한다.
leftButton.signal.clicked.connect(this.go_left);
centerButton.signal.clicked.connect(this.go_center);
rightButton.signal.clicked.connect(this.go_right);
여기서 draw_indicator 프로퍼티를 false로 설정하면 라디오 버튼의 모양이 일반 버튼과 같아질 것이다.
leftButton.draw_indicator = centerButton.draw_indicator = rightButton.draw_indicator = false;
아래는 목업의 첫 번째 상태에 표시된 바와 같이 중간 버튼을 활성화하기 위함이다.
centerButton.active = true;
아래 부분은 버튼 안에 라벨의 정렬을 설정한다. 0.5 값은 라벨 모두 중앙으로 정렬됨을 의미한다.
leftButton.xalign = 0.5;
centerButton.xalign = 0.5;
rightButton.xalign = 0.5;
이후 Fill 프로퍼티만 true로 설정되도록 하여 버튼을 패킹한다.
나머지 코드 부분은 GtkBuilder가 가능한(GtkBuilder-enabled) 코드와 비슷하다. 여기서는 수동 GTK+ 프로그래밍이 GTK+ API에 관해 더 많은 정보를 제공할 수 있음을 확인할 수 있다.
buttonBox.pack_start(leftButton, false, true, 0);
buttonBox.pack_start(centerButton, false, true, 0);
buttonBox.pack_start(rightButton, false, true, 0);
프로젝트의 .ui 파일이 커질수록 시동 시 파일을 로딩하는 데에 시간이 더 소요된다. 이런 경우 수동으로 패킹하는 방도를 고려하는 것도 좋은데, 특히 .ui 파일이 충분히 안정적이어서 개발 중에 많이 변경되지 않을 경우 그렇다. 반면 .ui 파일이 계속 변경될 경우 Glade와 GtkBuilder의 사용을 고려하는 것이 나은데, 수동 GTK+는 종종 머리를 어지럽게 만들기도 하기 때문이다. 불행히도 API 자체가 그다지 직관적이지 못하고 쉽게 오용되기 때문이다.
하지만 사용자 인터페이스를 깔끔하고 잘 구조화된 채로 유지하는 것이 좋은데, 그래야 어떤 방법이든 선택할 수 있기 때문이다. 목업을 이용해 어떤 위젯을 이용할 것인지 미리 계획하는 것도 좋은 실습이 되겠다.
Clutter를 이용한 GUI 프로그래밍
Clutter는 풍부한 애니메이션과 효과적인 기능을 제공한다는 이유로 주로 좀 더 강력한 GUI 애플리케이션을 생성하는 데에 사용되며, 종종 OpenGL을 이용한 렌더링에 사용되기도 한다. 아직은 OpenGL 특정적인 프로그래밍은 모두 개발자들에게 숨긴다. GTK+와 달리 Clutter는 scene graph 기반의 캔버스로, 원하는 장소로 무엇이든 놓을 수 있도록 해준다. 스테이지(stage) 상의 모든 객체는 2D 평면이지만 스테이지 자체는 3D 공간이다.
실행하기 - Clutter를 이용해 목업 구현하기
Clutter를 이용해 목업과 그 상호작용 흐름을 구현함으로써 Clutter를 소개하겠다.
- clutter-basic-vala라고 불리는 디렉터리에 clutter-basic.js라고 불리는 새로운 JavaScript를 생성하라.
- 아래 코드로 스크립트를 채워라.
#!/usr/bin/env seed Clutter = imports.gi.Clutter; Pango = imports.gi.Pango; GObject = imports.gi.GObject; Main = new GType({ parent: GObject.Object.type, name: "Main", init: function(self) { var stageColor = new Clutter.Color(); stageColor.from_string("#b0b0b0"); var labelColor = new Clutter.Color(); labelColor.from_string("#000000"); var buttonColor = new Clutter.Color(); buttonColor.from_string("#505050"); var buttonPressedColor = new Clutter.Color(); buttonPressedColor.from_string("#a0a0a0"); var buttonTextColor = new Clutter.Color(); buttonTextColor.from_string("#000000"); var buttonLeft = new Clutter.Rectangle(); buttonLeft.width = 100; buttonLeft.height = 40; buttonLeft.x = 0; buttonLeft.y = 360; buttonLeft.color = buttonColor; buttonLeft.set_border_color(stageColor); buttonLeft.set_border_width(1); var buttonCenter = new Clutter.Rectangle(); buttonCenter.width = 100; buttonCenter.height = 40; buttonCenter.x = 100; buttonCenter.y = 360; buttonCenter.color = buttonColor; buttonCenter.set_border_color(stageColor); buttonCenter.set_border_width(1); var buttonRight = new Clutter.Rectangle(); buttonRight.width = 100; buttonRight.height = 40; buttonRight.x = 200; buttonRight.y = 360; buttonRight.color = buttonColor; buttonRight.set_border_color(stageColor); buttonRight.set_border_width(1); var s = Clutter.Stage.get_default(); s.color = stageColor; this.s = s; s.width = 300; s.height = 400; var fd = Pango.FontDescription.from_string("Sans 16"); var label = new Clutter.Text(); label.set_font_description(fd); label.set_text("in the center"); label.color = labelColor; label.x = (s.width - label.width)/2; label.y = 100; var buttonFd = Pango.FontDescription.from_string("Sans 12"); var buttonLeftText = new Clutter.Text(); buttonLeftText.set_font_description(buttonFd); buttonLeftText.set_text("Go Left"); buttonLeftText.color = buttonTextColor; buttonLeftText.x = (buttonLeft.width - buttonLeftText.width) /2; buttonLeftText.y = (buttonLeft.height - buttonLeftText.height) /2 + buttonLeft.y; var buttonCenterText = new Clutter.Text(); buttonCenterText.set_font_description(buttonFd); buttonCenterText.set_text("Go Center"); buttonCenterText.color = buttonTextColor; buttonCenterText.x = (buttonCenter.width - buttonCenterText.width) /2 + buttonLeft.width; buttonCenterText.y = (buttonCenter.height - buttonCenterText.height) /2 + buttonCenter.y; var buttonRightText = new Clutter.Text(); buttonRightText.set_font_description(buttonFd); buttonRightText.set_text("Go Right"); buttonRightText.color = buttonTextColor; buttonRightText.x = (buttonRight.width - buttonRightText.width) /2 + buttonLeft.width + buttonCenter.width; buttonRightText.y = (buttonRight.height - buttonRightText.height) /2 + buttonRight.y; buttonLeft.set_reactive(true); buttonLeft.signal.button_press_event.connect(function(self) { buttonLeft.color = buttonPressedColor; buttonRight.color = buttonCenter.color = buttonColor; label.save_easing_state(); label.set_text("going left"); label.set_x(0); label.restore_easing_state(); return true; }); buttonCenter.set_reactive(true); buttonCenter.signal.button_press_event.connect(function(self) { buttonCenter.color = buttonPressedColor; buttonRight.color = buttonLeft.color = buttonColor; label.save_easing_state(); label.set_text("in the center"); label.set_x((s.width - label.width)/2); label.restore_easing_state(); return true; }); buttonRight.set_reactive(true); buttonRight.signal.button_press_event.connect(function(self) { buttonRight.color = buttonPressedColor; buttonLeft.color = buttonCenter.color = buttonColor; label.save_easing_state(); label.set_text("going right"); label.set_x(s.width - label.width); label.restore_easing_state(); return true; }); buttonCenter.color = buttonPressedColor; s.add_actor(buttonLeft); s.add_actor(buttonRight); s.add_actor(buttonCenter); s.add_actor(buttonLeftText); s.add_actor(buttonRightText); s.add_actor(buttonCenterText); s.add_actor(label); s.show_all(); } }); Clutter.init(Seed.argv); var main = new Main(); Clutter.main();
- JavaScript 코드 대신 Vala를 원한다면 Anjuta에서 프로젝트를 생성할 때 non-GUI 애플리케이션 설정과 유사한 설정을 이용해 새로운 Vala 프로젝트를 생성해보자. clutter-basic-vala라는 새 프로젝트를 생성해보자.
- src/Makefile.am 파일을 편집하고 아래 부분을 찾아라.
clutter_basic_vala_VALAFLAGS = \ --pkg gtk+-3.0
두 행을 아래의 내용으로 편집하라.clutter_basic_vala_VALAFLAGS = \ --pkg clutter-1.0
- configure.ac 파일을 편집하고 아래 행을 찾아라.
PKG_CHECK_MODULES(CLUTTER_BASIC_VALA, [gtk+-3.0])
전체 행을 아래 행으로 대체하라.PKG_CHECK_MODULES(CLUTTER_BASIC_VALA, [clutter-1.0])
- 본문에서 코드를 반복해서 싣는 것을 피하기 위해 JavaScript 코드를 Vala로 이식해보자. 앞에서 두 개의 프로그래밍 언어를 소개한 바가 있기 때문에 꽤 수월한 작업이다.
- 프로그램을 실행하면 아래 스크린샷과 같은 모습이 될 것이다.
무슨 일이 일어났는가?
코드의 양이 많다는 것은 단숨에 알아볼 수 있다. 이는 Clutter가 GTK+보다 저수준이기 때문이다. 이것은 매우 기본적인 위젯만 갖고 있기 때문에 처음부터 고유의 위젯을 생성해야 한다. Clutter가 제공하는 함축적 애니메이션 때문에 상호작용은 좀 더 유동적이고 만족스럽다.
기술적 측면에서 보면 객체가 스테이지 어디든 위치할 수 있음을 확인할 수 있다. Clutter에서 객체는 행위자(actors) 라고 부른다. 애니메이션의 역할이나 효과를 행위자로 적용할 수 있고 스테이지 주위로 이동시킬 수 있다.
JavaScript와 비교할 때 Vala로 Clutter를 코딩할 때는 시작 시 약간의 준비가 필요하다. configure.ac와 Makefile.am 파일을 수정함으로써 빌드 시스템을 조정할 필요가 있다.
이러한 조정을 거치면 빌드 시스템은 Vala에서 컴파일할 때나 생성된 C 코드 파일을 만들 때 Clutter 라이브러리를 익히게 된다. 우리는 Makefile.am과 configure.ac에서 gtk+-3.0 설정을 제거함으로써 GTK+ 라이브러리를 제거하고 이를 clutter-1.0으로 대체하는 일을 수행한 것이다. 그 이유는 이번 예제에서 GTK+가 전혀 필요하지 않기 때문이다. JavaScript를 이용하면 자동으로 작동할 것이기 때문에 우리는 어떤 일도 수행할 필요가 없으므로 훨씬 더 수월하다.
Clutter가 어떻게 작동하는지 이해하도록 코드를 좀 더 깊이 살펴보자.
메인 코드는 꽤 간단하고 GTK+의 메인 코드와 유사하다. init와 메인 루프 함수가 있다.
Clutter.init(Seed.argv);
var main = new Main();
Clutter.main();
먼저 애플리케이션에서 사용되는 색상 몇 가지를 정의한다. 이는 16진 RGB 색상 포맷을 파싱한 후 변수로 저장함을 확인할 수 있다.
var stageColor = new Clutter.Color();
stageColor.from_string("#b0b0b0");
이것을 우리의 버튼 위젯이라고 상상해보자. 코드를 간소화하기 위해 크기와 위치를 하드코딩한 후 색상을 앞서 설정한 색상 중 하나로 설정한다.
실제 애플리케이션에서 크기와 위치는 계산 또는 사전 정의된 설정으로부터 비롯되어야 한다.
var buttonLeft = new Clutter.Rectangle();
buttonLeft.width = 100;
buttonLeft.height = 40;
buttonLeft.x = 0;
buttonLeft.y = 360;
buttonLeft.color = buttonColor;
buttonLeft.set_border_color(stageColor);
buttonLeft.set_border_width(1);
이 부분은 모든 위젯을 위치시킬 스테이지를 정의한다. 스테이지는 앞서 소개한 GtkWindow 위젯에 비할 수 있다.
var s = Clutter.Stage.get_default();
s.color = stageColor;
this.s = s;
s.width = 300;
s.height = 400;
이 코드 부분은 글꼴 설명을 정의한다. 문자열로부터 설명을 취하여 후에 라벨이 사용하게 될 글꼴의 논리적 표현을 리턴한다.
var fd = Pango.FontDescription.from_string("Sans 16");
그리고 라벨의 텍스트, 글꼴, 색상, 위치를 정의한다.
var label = new Clutter.Text();
label.set_font_description(fd);
label.set_text("in the center");
label.color = labelColor;
label.x = (s.width - label.width)/2;
label.y = 100;
여기서는 버튼의 텍스트를 정의한다. 모습이 개선되었음을 확인할 수 있는데, 즉 텍스트의 위치를 더 이상 하드코딩하지 않는다는 점이 확인된다.
var buttonFd = Pango.FontDescription.from_string("Sans 12");
var buttonLeftText = new Clutter.Text();
buttonLeftText.set_font_description(buttonFd);
buttonLeftText.set_text("Go Left");
buttonLeftText.color = buttonTextColor;
buttonLeftText.x = (buttonLeft.width - buttonLeftText.width) /2;
buttonLeftText.y = (buttonLeft.height - buttonLeftText.height) /2 +
buttonLeft.y;
다음으로 버튼을 반응(reactive)하도록 설정한다. 이 코드가 없다면 버튼은 들어오는 이벤트에 반응할 수 없을 것이다.
buttonLeft.set_reactive(true);
아래는 button-press-event 이벤트의 핸들러를 정의한다. 핸들러에서는 누름 버튼을 설정하고 눌러진 느낌을 표현하기 위해 다른 버튼의 색상을 리셋한다. 라벨의 프로퍼티를 변경하고 후에 이를 복구하기 전에 easing 상태를 저장함으로써 함축적 애니메이션을 요청한다. 함축적 애니메이션이란 코드에서 우리가 특정 타입의 애니메이션을 구체적으로 요청하지 않음을 의미한다. 대신 우리가 행위자에서 프로퍼티를 변경할 때마다 Clutter가 애니메이션을 실행하도록 의존한다.
buttonLeft.signal.button_press_event.connect(function(self) {
buttonLeft.color = buttonPressedColor;
buttonRight.color = buttonCenter.color = buttonColor;
label.save_easing_state();
label.set_text("going left");
label.set_x(0);
label.restore_easing_state();
return true;
});
값을 변경하자마자 Clutter는 값을 목표값(target value)으로 취급하고, 타이머를 준비하여 타이머의 각 프레임마다 위젯을 향한 프로퍼티의 현재 값을 증가(또는 감소)시킬 것이다. 이 모든 것은 배경에서 자동으로 발생한다.
actor 라벨에서 Clutter는 애니메이션에 두 가지, 즉 텍스트와 텍스트의 위치에 변경내용을 적용한다. 위치는 텍스트의 새로운 너비를 필요로 하는데 이는 텍스트 값을 설정한 다음에 이용할 수 있으므로 이번 예제에서는 위치를 변경하기 전에 텍스트를 설정해야 함을 명심하라. 순서를 바꾸면 라벨의 위치는 올바르지 않은 값이 될 것이다.
한 가지 관찰해야 할 사항이 있는데, 시그널에 대해 반응할 때 무언가 조치를 취할 때마다 함수가 true를 리턴한다는 점이다. 혹시 시그널을 건너뛰거나 시그널에 대한 조치를 거부할 경우에는 false를 리턴해야만 큐에 있는 또 다른 이벤트 핸들러가 시그널을 처리할 것이다.
s.add_actor(buttonLeft);
s.add_actor(buttonRight);
s.add_actor(buttonCenter);
s.add_actor(buttonLeftText);
s.add_actor(buttonRightText);
s.add_actor(buttonCenterText);
s.add_actor(label);
여기서는 스테이지에 모든 행위자를 추가하여 아래와 같이 표시되도록 한다.
s.show_all();
마지막으로 모든 행위자와 스테이지를 함께 표시한다.
시도해보기 - 애니메이션 갖고 놀기
버튼이나 라벨에는 수많은 변화를 적용할 수 있다. 예를 들어, 버튼을 누르면 버튼의 크기를 증가시킬 수 있고 버튼 누름을 해제하면 버튼이 다시 본래 크기로 작아지도록 만들 수 있다. 그리고 이 모든 것이 애니메이션으로 만들어질 것이다. button-release-event 이벤트를 처리하도록 더 많은 클로저를 추가하여 애니메이션으로 만들고자 하는 행위자의 본래 값을 복구할 수도 있다.
요약
이번 장에서는 GTK+와 Clutter를 이용한 GUI 애플리케이션의 생성과 관련해 학습하였다. 각 툴킷에 대한 시스템 요구사항에는 차이가 있음을 이해하였고, 이러한 툴킷을 이용해 생성할 수 있는 애플리케이션의 유형도 학습하였다.
GTK+를 이용하면 사용할 준비가 된 위젯이 제공된다는 사실도 학습하였다. 그 중에서도 Button, Box, Window, Label의 사용법을 익혔다. 위젯의 정렬을 설정하고, 그들의 프로퍼티를 설정함으로써 관리하는 방법도 학습하였다. 이벤트에 반응하고 유용한 일을 실행할 핸들러로 이벤트를 연결하는 방법도 알게 되었다.
Clutter를 이용하면 만족스러운 시각적 애니메이션이 있는 애플리케이션을 생성할 수 있음을 배웠다. 하지만 여기에도 몇 가지 한계가 있다. 그 중 하나로, 애플리케이션을 만들 때 용이하게 사용하기엔 너무 기본적인 기반만 제공한다는 점을 들 수 있다.
그 외에도 목업과 상호작용 흐름을 이용하면 GUI 애플리케이션의 개발이 훨씬 수월해질 수 있음을 배웠다. 또 애플리케이션에 어떤 위젯을 사용할 것인지 미리 계획할 수 있음을 알게 되었다.
다음 장에서는 자신만의 위젯을 생성함으로써 GTK+를 확장하는 법을 학습할 것이다.