다음 이전 차례

3. 시작하면서

먼저 할 일은 물론 GTK의 소스를 받아서 그것을 설치하는 것이다. 언제나 가장 최신 버전을 ftp.gtk.org의 /pub/gtk에서 가져올 수 있다. GTK에 대한 또다른 소스들은 http://www.gtk.org/에 있다. GTK는 GNU autoconf를 이용한다. 일단 압축을 푼 다음에, ./configure --help를 쳐서 옵션들을 살펴 보기 바란다.

GTK에 대한 소개를 위해서, 가능한 한 간단한 프로그램부터 시작해 보자. 이 프로그램은 200x200 픽셀의 윈도를 만드는 것으로, 쉘을 이용하지 않고는 끝낼 수 없는 프로그램이다.

#include <gtk/gtk.h>

int main (int argc, char *argv[])
{
    GtkWidget *window;

    gtk_init (&argc, &argv);

    window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
    gtk_widget_show (window);

    gtk_main ();

    return 0;
}

모든 프로그램은 물론 GTK 어플에 쓰일 변수, 함수, 구조체 등이 선언되어 있는 gtk/gtk.h를 포함할 것이다.

gtk_init (&argc, &argv);

GTK로 쓰여지는 다른 모든 프로그램에서 부르게 되는 함수인 gtk_init(gint *argc, gchar ***argv) 를 부르고 있다. 이것은 디폴트로 쓸 비주얼과 칼라맵 등 몇가지를 세팅하며 그 이후 gdk_init(gint *argc, gchar ***argv)로 넘어간다. 이 함수는 쓰일 라이브러리를 초기화 하고, 디폴트로 시그널 핸들러를 셋업하며, 명령행을 통해 프로그램에 전해진 인자들 중 아래의 것들을 찾아 체크한다.

당신의 어플이 인식하지 않을 것들을 남겨두고, 위의 것들을 인자의 리스트에서 제거한다. 이것은 모든 GTK 어플에서 생략할 표준적인 인자들의 세트를 만든다.

다음의 두 줄에서는 윈도를 만들고 보여준다.

window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
gtk_widget_show (window);

GTK_WINDOW_TOPLEVEL이란 인자는 윈도 매니저의 장식과 위치설정에 따르게 한다. 0x0 크기의 윈도를 만들지 않고, child가 없는 윈도는 디폴트로 200x200 크기로 만들어져서 우리는 그것을 마음껏 다룰 수 있다.

gtk_widget_show() 함수는 이 widget의 속성에 대한 세팅이 끝났음을 GTK에 알려주는 것이고, 그래서 그 widget은 보여지게 된다.

마지막 줄은 GTK의 주 처리 루프에 들어가는 것이다.

gtk_main ();

gtk_main()은 GTK 어플 전반적으로 볼 수 있는 또 하나의 함수호출이다. 프로그램이 여기에 이르면, GTK는 X 이벤트, 타임아웃, 또는 파일 입출력 감지 등을 기다리며 대기하게 된다. 그러나 이 간단한 프로그램에서는 그런 것들이 무시되고 있다.

3.1 GTK에서의 Hello World 프로그램

이제 하나의 widget(버튼)을 위한 프로그램이다, 그 유명한 hello world를 GTK로써 구현해 보자.

/* helloworld.c */

#include <gtk/gtk.h>

/* 이것은 callback함수다.  여기서는 데이터인자들이 무시되었다. */
void hello (GtkWidget *widget, gpointer data)
{
    g_print ("Hello World\n");
}

gint delete_event(GtkWidget *widget, GdkEvent *event, gpointer data)
{
    g_print ("delete event occured\n");
    /* "delete_event"라는 시그널핸들러에서 TRUE를 리턴하면,
     * GTK는 "destroy" 시그널을 발생시킨다.  FALSE를 리턴하면
     * 윈도가 진짜로 파괴되길 원하지 않는 것이다.  이것은
     * '진짜로 끝내렵니까?' 같은 대화상자가 튀어나오게 할 때
     * 유용하다. */
       
    /* FALSE를 TRUE로 고치면 main 윈도는 "delete_event"와 함께
     * 파괴될 것이다. */
    return (FALSE);
}

/* 또다른 callback */
void destroy (GtkWidget *widget, gpointer data)
{
    gtk_main_quit ();
}

int main (int argc, char *argv[])
{
    /* GtkWidget은 widget들을 위한 기억장소 타입이다. */
    GtkWidget *window;
    GtkWidget *button;

    /* 이것은 모든 GTK 어플들에서 호출된다.  인자들은 명령행 상으로부터
     * 주어져 어플로 전해진다. */
    gtk_init (&argc, &argv);

    /* 새로운 윈도를 만든다. */
    window = gtk_window_new (GTK_WINDOW_TOPLEVEL);

    /* 윈도가 "delete_event"를 받으면(이는 윈도매니저를 통한다), 우리는
     * 위에서 정의한 함수 delete_event()를 호출할 것인지 확인한다.  이
     * callback함수로 전해진 데이터는 NULL이고 따라서 여기서 무시된다. */
    gtk_signal_connect (GTK_OBJECT (window), "delete_event",
                        GTK_SIGNAL_FUNC (delete_event), NULL);

    /* 여기서는 시그널핸들러에 "destroy" 이벤트를 연결해 준다.
     * 이 이벤트는 우리가 윈도에 대해 gtk_widget_destroy()를 호출하거나,
     * 또는 "delete_event" callback에서 'TRUE'를 리턴했을 때 발생한다. */
    gtk_signal_connect (GTK_OBJECT (window), "destroy",
                        GTK_SIGNAL_FUNC (destroy), NULL);

    /* 윈도의 border width를 세팅한다. */
    gtk_container_border_width (GTK_CONTAINER (window), 10);

    /* "Hello World"라는 라벨이 있는 새로운 버튼을 만든다. */
    button = gtk_button_new_with_label ("Hello World");

    /* 버튼이 "clicked" 시그널을 받으면 NULL을 인자로 해서 hello()가
     * 호출된다.  이 hello() 함수는 위에서 정의되었다. */
    gtk_signal_connect (GTK_OBJECT (button), "clicked",
                        GTK_SIGNAL_FUNC (hello), NULL);

    /* 이것이 "clicked"되면 gtk_widget_destroy(window)가 호출, 그 윈도를
     * 파괴하게 된다.  앞서 설명했듯이, 'destroy' 시그널은 여기서 이렇게
     * 발생하거나, 또는 윈도매니저를 통해 발생한다. */
    gtk_signal_connect_object (GTK_OBJECT (button), "clicked",
                               GTK_SIGNAL_FUNC (gtk_widget_destroy),
                               GTK_OBJECT (window));

    /* 이것은 버튼을 윈도, 즉 GTK 컨테이너의 하나로 패킹한다. */
    gtk_container_add (GTK_CONTAINER (window), button);

    /* 끝으로 새로 만든 widget인 버튼을 보여주는 것이다. */
    gtk_widget_show (button);

    /* 윈도 전체를 여기서 보여준다. */
    gtk_widget_show (window);

    /* 모든 GTK 어플은 꼭 gtk_main()을 하나씩 가지고 있다.  컨트롤은
     * 여기서 끝나고 이제 어떤 이벤트(키를 누른다거나 마우스를 조작하는
     * 등의)가 발생할 때까지 대기한다. */
    gtk_main ();

    return 0;
}

3.2 Hello world를 컴파일하기

컴파일하기 위해 이렇게 하라.

gcc -Wall -g helloworld.c -o hello_world `gtk-config --cflags` \
    `gtk-config --libs`

이렇게 하는 것은 gtk와 함께 딸려오는 gtk-config란 프로그램을 이용하는 것이다. 이 프로그램은 gtk를 쓰는 프로그램을 컴파일하기 위해서 어떤 컴파일러 스위치들이 필요한지 '알고'있다. gtk-config --cflags 는 컴파일러가 header 파일들을 찾아보아야 하는 include 디렉토리들을 출력하고, gtk-config --libs 는 링크되어야 하는 라이브러리들과 그 라이브러리들이 존재하는 디렉토리들을 출력한다.

보통 링크시키는 라이브러리들은 다음과 같은 것들이다.

3.3 시그널과 callback에 대한 이론

Hello world 프로그램을 자세히 살펴보기에 앞서, 이벤트와 callback에 대해 집고 넘어가자. GTK는 이벤트로 돌아가는 툴킷이며, 이것은 어떤 이벤트가 발생해서 적절한 다른 함수로 제어가 넘어가지 않는 한 계속 대기한다는 의미다.

이 제어를 넘긴다는 것은 "시그널"의 이용에 의해 이루어진다. 마우스 버튼을 누르는 것같이 어떤 이벤트가 있으면, 눌러졌다에 해당하는 적절한 시그널이 widget에 의해 "발생"하게 된다. 이것이 GTK가 하는 유용한 작업의 대부분이다. 버튼의 작용으로 어떤 동작을 시키려면, 우리는 이런 시그널들을 잡아내도록 시그널 핸들러를 셋업하고 이에 적당한 함수를 부르면 된다. 이것은 다음과 같은 함수를 이용함으로써 이루어진다.

gint gtk_signal_connect (GtkObject *object,
                         gchar *name,
                         GtkSignalFunc func,
                         gpointer func_data);

첫번째 인자는 시그널을 발생시킬 widget이고, 두번째는 잡아내고자 하는 시그널의 이름이다. 세번째 인자는 그 시그널이 탐지되었을 때 호출할 함수며, 네번째는 이 함수에 넘겨줄 정보다.

세번째 인자로 나와있는 함수를 "callback 함수"라고 부르며 다음처럼 이루어져 있다.

void callback_func(GtkWidget *widget, gpointer callback_data);

이 함수의 첫번째 인자는 시그널을 발생시킨 widget을 향한 포인터가 되고, 두번째 인자는 위에 보인 gtk_signal_connect() 함수의 네번째 인자로 주어져 있는 정보에 대한 포인터다.

Hello world 예제에서 쓰인 또다른 호출은 이것이다.

gint gtk_signal_connect_object (GtkObject *object,
                                gchar  *name,
                                GtkSignalFunc func,
                                GtkObject *slot_object);

gtk_signal_connect_object()는 callback함수가 GTK object에 대한 포인터라는, 단 하나의 인자를 이용한다는 점만 빼고는 gtk_signal_connect()와 같다. 그러므로 시그널을 연결시킬 때 이 함수를 이용한다면 callback함수는 이런 모양을 해야 할 것이다.

void callback_func (GtkObject *object);

여기서의 object란 대개 widget을 말한다. 우리는 어쨌든 되도록 gtk_signal_connect_object를 이용해서 callback을 셋업하지는 않을 것이다. 그들은 우리의 hello world 예제에서처럼, 주로 인자로서 시그널 widget/object를 허용하는 GTK 함수들을 부를 때 쓰이고 있다.

시그널을 연결시키는 데 두 개의 함수를 가지고 있는 것은 단지 서로 다른 갯수의 인자를 가지고 있는 callback을 허용하기 위해서이다. GTK 라이브러리에 있는 많은 함수들은 인자로서 오직 GtkWidget 포인터만을 허용하므로, 이런 경우 에는 gtk_signal_connect_object() 를 이용하면 될 것이다. 반면 자신의 함수에 대해서는, callback에 추가적인 정보를 가져야 할 필요가 있을 것이다.

3.4 Hello World를 따라 한걸음씩

이제 적용된 이론에 대해 알았고, hello world 예제 프로그램을 따라 더 확실히 해두자.

이것은 버튼의 "눌림"이라는 이벤트에 대해 호출될 callback함수다. 이 예제 에서는 widget과 정보가 모두 무시되지만, 그것들을 다루는 것은 그다지 어렵지 않다. 이것 다음의 예제는 어떤 버튼이 눌러졌는가를 알려주기 위해 정보 인자를 이용하는 것이다.

void hello (GtkWidget *widget, gpointer data)
{
    g_print ("Hello World\n");
}

이 callback은 다소 특별하다. 윈도매니저가 어플에 이 이벤트를 보내면 "delete_event"가 발생하게 된다. 우리는 여기서 이런 이벤트들에 대해 무엇을 해줘야 할지 선택한다. 우리는 그것을 무시할 수도 있고, 이에 대한 반응을 정리할 수도 있으며, 또는 간단히 그 어플을 종료할 수도 있다.

이 callback의 리턴값을 통해 GTK는 어떤 일을 해야할 지 알게 된다. FALSE를 리턴하면 GTK는 우리가 "destroy"시그널을 발생하는 것을 원하지 않는다고 판단 하게 된다. 그리고 TRUE를 리턴하는 것으로 우리는 "destroy"시그널이 발생함을 확인시키고, "destroy"시그널 핸들러를 호출하게 된다.

gint delete_event(GtkWidget *widget, GdkEvent *event, gpointer data)
{
    g_print ("delete event occured\n");

    return (FALSE);
}

여기 단지 gtk_main_quit()을 호출하는 것으로 종료해 버리는, 또다른 callback 함수가 있다. 이것에 대해선 긴말 않겠다, 보면 알 수 있을 것이므로.

void destroy (GtkWidget *widget, gpointer data)
{
    gtk_main_quit ();
}

당신이 main() 란 것에 대해 알고 있으리라 생각한다, 다른 어플과 마찬가지로, 모든 GTK 어플 역시 main() 을 하나씩 가지고 있다.

int main (int argc, char *argv[])
{

이 부분에서, GtkWidget형의 구조체에 대한 포인터를 선언한다. 이것들은 윈도와 버튼을 만들기 위해 아래에서 쓰이게 된다.

    GtkWidget *window;
    GtkWidget *button;

여기 다시 gtk_init가 나온다. 전과 마찬가지로, 이것은 툴킷을 초기화하고, 명령행에서 주어진 인자들을 분석한다. 이것은 명령행에서 감지된 어떤 인자라도 인자의 리스트에서 삭제하고, argc와 argv를 변형시켜 그 인자들이 존재하지 않는 것처럼 보이게 해서, 당신의 어플이 남아있는 인자들만을 분석하도록 해준다.

    gtk_init (&argc, &argv);

새 윈도를 하나 만든다. 이것은 꽤나 간단하다. GtkWidget *window 가 가리 키는 구조체에 메모리가 할당되어 이제 실제로 존재하는 구조체를 가리키게 된 것이다. 새로운 윈도를 셋업하지만, 아직 gtk_widget_show(window)를 호출하지 않았기 때문에 실제로 보여지진 않고 있다. 이 윈도는 프로그램의 끝 근처에서야 보이게 될 것이다.

    window = gtk_window_new (GTK_WINDOW_TOPLEVEL);

이 경우엔 윈도가 되겠지만, 여기 한 object에 시그널 핸들러를 연결시키는 예가 있다. 여기서는 "destory"시그널이 탐지된다. 이 시그널은 우리가 윈도를 없애기 위해서 윈도매니저의 기능을 이용했을 때(그리고 우리가 "delete_event" 핸들러에서 TRUE를 리턴했을 때), 또는 파괴할 object로 윈도widget을 선택하여 gtk_widget_destroy()를 호출했을 때 발생한다. 이것을 셋업하는 것으로 우리는 한 번의 호출로 두 경우 모두를 다룰 수 있다. 여기서, 그것은 앞에서 정의된 destroy() 함수를 NULL 인자로 호출하여 GTK를 종료하게 한다.

GTK_OBJECT와 GTK_SIGNAL_FUNC는 코드를 쉽게 분석할 수 있게 해주며 자료형 캐스팅과 체크를 해주는 매크로들이다.

    gtk_signal_connect (GTK_OBJECT (window), "destroy",
                        GTK_SIGNAL_FUNC (destroy), NULL);

이번 함수는 container object의 속성을 설정해 주기 위해 쓰였다. 이것은 윈도를 10 픽셀의 너비의 빈 영역으로 둘러쌓이게 한다. 우리는 앞으로 Widget 속성설정 이라는 section에서 유사한 기능을 가진 다른 함수들을 접하게 될 것이다.

그리고, GTK_CONTAINER는 역시 자료형 캐스팅을 해주는 매크로다.

    gtk_container_border_width (GTK_CONTAINER (window), 10);

이 호출은 새 버튼을 하나 만든다. 이것은 새로운 GtkWidget 구조체를 위한 메모리 공간을 할당하고, 그것을 초기화하며, 그리고 button이라는 포인터가 그 영역을 가리키도록 한다. 이 버튼은 보여지게 되었을 때 "Hello World"라는 라벨을 가지게 될 것이다.

    button = gtk_button_new_with_label ("Hello World");

우리는 여기서 이 버튼이 뭔가 유용한 일을 하도록 한다. 우리는 그것에 시그널 핸들러를 달아주고, "clicked"시그널이 발생했을 때 우리의 hello() 함수가 호출되도록 한다. 전해질 정보는 없고, 따라서 우리는 hello()라는 callback함수에 단순히 NULL을 전해준다. 분명히, 우리가 마우스 포인터로 그 버튼을 클릭했을 때 "clicked"시그널이 발생하게 된다.

    gtk_signal_connect (GTK_OBJECT (button), "clicked",
                        GTK_SIGNAL_FUNC (hello), NULL);

우리는 또한 프로그램을 끝내기 위해서도 이 버튼을 이용한다. 이것은 윈도 매니저를 통해서, 혹은 우리 프로그램을 통해서 "destroy"시그널이 어떻게 전해 오는지를 보여줄 것이다. 앞에서처럼 버튼이 눌려지면 먼저 hello() callback 함수가 불려지고, 뒤이어 그들이 셋업된 순서대로 이것이 호출된다. 필요하다면 얼마든지 callback함수를 쓸 수 있으며, 그것들 모두는 우리가 연결시켜놓은 순서대로 실행된다. gtk_widget_destroy()함수가 오직 GtkWidget *widget만을 인자로 허용하기 때문에, 우리는 여기서 gtk_signal_connect()를 바로 쓰지 않고 gtk_signal_connect_object()를 쓴다.

    gtk_signal_connect_object (GTK_OBJECT (button), "clicked",
                               GTK_SIGNAL_FUNC (gtk_widget_destroy),
                               GTK_OBJECT (window));

이것은 패킹호출이며 뒤에서 더 깊게 다룰 것이다. 하지만 상당히 쉽게 이해할 수 있다. 이것은 단지 GTK에게, 버튼은 그 버튼이 보여지게 될 윈도에 위치해야 된다는 걸 알려준다.

    gtk_container_add (GTK_CONTAINER (window), button);

이제 우리는 필요한 모든 셋업을 마쳤다. 자리를 잡은 모든 시그널 핸들러와 함께, 모든 버튼이 각각 있어야 할 윈도에 자리를 잡았고, 우리는 GTK가 스크린 위에 widget들을 "보여주기"를 요구한다. 윈도widget이 가장 나중에 보여진다. 따라서 윈도가 먼저 보여지고 그 내부에 버튼 모양이 뒤이어 그려지는 게 아니라, 완전히 모양을 갖춘 윈도가 한꺼번에 보여지게 된다.

    gtk_widget_show (button);

    gtk_widget_show (window);

그리고 물론, 우리는 gtk_main()을 불러서 X 서버로부터 이벤트가 발생하기를 기다리게 하고, 이 이벤트가 탐지되었을 때 시그널을 내도록 widget을 호출할 것이다.

    gtk_main ();
그리고 마지막 리턴. gtk_quit()가 호출된 이후 제어는 여기서 끝나게 된다.
    return 0;

이제, 우리가 GTK의 버튼 위에서 마우스 버튼을 클릭하면 widget은 "clicked" 시그널을 발생시킨다. 이런 정보를 이용하기 위해 우리 프로그램은 그 시그널을 잡아낼 시그널 핸들러를 셋업하고, 그것은 우리의 선택에 의한 함수들을 재빨리 부르게 된다. 우리의 예제에서는 우리가 만든 버튼이 눌려지면 hello()함수가 NULL 인자로 호출되고, 뒤이어 이 시그널을 위한 다음 핸들러가 호출된다. 이것은 윈도 widget을 인자로 하여 gtk_widget_destroy()함수를 호출해서 윈도 widget을 없앤다. 이것은 윈도가 "destroy"시그널을 발생하게 해서 탐지되고, "destroy"에 해당하는 callback함수를 호출해서 GTK가 종료되게 하는 것이다.

이벤트들의 또다른 사용방법은 윈도를 종료하는데 윈도 매니져를 쓰는 것이다. 이렇게 하면 "delete_event" 시그널이 발생하고 이는 우리의 "delete_event" 핸들러를 호출한다. 만일 우리가 여기서 TRUE를 리턴하면 윈도는 그대로 화면에 남고 아무일도 생기지 않는다. FALSE를 리턴하면 GTK는 "destroy" 시그널을 발생하고 "destroy" callback이 호출되어 GTK가 종료된다.

비록 쓰임새는 거의 같은 것이지만, 유닉스 시스템의 시그널은 여기서 말하는 이런 시그널과 다른 것이며 사용되지도 않는다는 점을 기억하기 바란다.


다음 이전 차례