대문‎ > ‎GNOME 배우기‎ > ‎

Example 어플리케이션 만들기

1. GtkWindow

GTK GUI 프로그램의 기본

GTK GUI프로그램은 대부분 먼저 GtkWindow부터 생성하고 GtkWindow위에 다른 위젯들을 배치하여 화면을 구성한다.

GtkWidget* gtk_window_new (GtkWindowType type)

GtkWindow를 생성하는 함수이다. 이미 설명했듯이 반환 타입은 GtkWidget의 포인터이다.

  GtkWidget *window;  window = gtk_window_new (GTK_WINDOW_TOPLEVEL);

GtkWindowType

GtkWindowType은 생성할 Window의 형식이다.

  typedef enum
  {
    GTK_WINDOW_TOPLEVEL,
    GTK_WINDOW_POPUP
  } GtkWindowType;

+ GTK_WINDOW_TOPLEVEL
일반적으로 사용하는 Window의 형식이다.

+ GTK_WINDOW_POPUP
팝업의 형식을 가지는 Window인데 여러분이 생각하는 팝업과는 다소 차이가 있을것이다. 우리가 흔히 팝업이라고 하면 Dialog를 생각하게 되는데 이와는 다르다.

GTK의 소스중 gtkwindow.c를 보면 이와같은 내용도 있다. 다른 툴킷에서의 팝업이 아니고 pop-up menu나 pop-up tooltip을 의미한다는 내용과 decorated가 false라는 것이다. 실제로도 TOPLEVEL과 별다른 차이가 없고 상단 타이틀이나 테두리만 없다. 본인도 사용한적이 없었던것 같다.

2. GtkLabel

GtkLabel은 이번엔 사용하지 않겠지만 기본적인 사항 정도만 함께 알아두자.

간단한 텍스트를 표시하는 Widget.

제목 및 입력 항목의 이름 등 간단한 텍스트를 화면에 표시하기 위해 사용하는 GUI 프로그램의 대표적인 Widget중의 하나이다. 여러줄의 텍스트를 표시할 수도 있고 양식을 Markup Language(Pango Text)를 이용하여 다중 형태의 텍스트도 표시할 수 있다.

GtkWidget* gtk_label_new (const gchar *str)

GtkLable를 생성하는 함수이다. 이미 설명했듯이 반환 타입은 GtkWidget의 포인터이다. 파라미터 str은 GtkLabel에 표시될 텍스트를 넣어주는데 표시할 텍스트가 없으면 NULL을 넣어주면 된다.

GtkWidget *label;
label = gtk_label_new (NULL);

void gtk_label_set_text (GtkLabel *label, const gchar *str)

GtkLabel의 표시될 텍스트를 변경한다.

gtk_label_set_text (GTK_LABEL (label), "Hi 똘츄~");

Pango Text Markup Language

GtkLable에는 Pango Text의 Markup Language를 이용하여 다중형태의 텍스트를 표시할 수 있다. 예를 들자면 Hi 까지는 노란색으로 하고 똘츄~는 초록색으로 하는것이 가능하다는 얘기이다. 그것도 하나의 위젯에서 말이다.

일단은 그런것이 있다는 정도만 알아두길 바란다. 앞으로도 갈길이 멀다 자세한것은 가다보면 알려주는 날이 있을 것이다. 그걸 알기 위해서는 Pango부터 간단히 알고 있는 것이 좋다.

급히먹은 밥은 체하기 마련이다.
(본인은 실제로 체하고서 많이 후회하는 사람중에 한명이다 --;)
천리길도 한걸음 부터 천천히 가보자.
가다보면 어느세 오십걸음 백걸음 삼백걸음... 천걸음에 다 도착하게 된다.

3. GtkButton

일반적인 형태의 버튼 Widget

일반적으로 가장 많이 사용하게 될 버튼이다. 이외에는 토글 버튼, 체크 버튼, 라디오 버튼, 링크 버튼 등이 있고 모두 GtkButton으로 부터 파생(상속) 되었다. 일반적으로 생각하는 버튼의 모양을 가진것은 버튼과 토글 버튼이 있다.

GtkWidget* gtk_button_new (void)

GtkButton을 생성하는 함수이다. 이미 설명했듯이 반환 타입은 GtkWidget의 포인터이다.

GtkWidget *button;
button = gtk_button_new ();

gtk_button_set_label (GTK_BUTTON (button), "Hi 똘츄~");

GtkButton의 Label에 표시되는 Text를 변경하는 함수이다.

GtkWidget* gtk_button_new_with_label (const gchar *label)

GtkButton을 생성하고 Label에 표시되는 Text를 변경하는 함수이다. gtk_button_set_label을 별도로 할 필요 없이 Widget 생성시 한꺼번에 해주는 것이다.

4. 사용할 Signal

"destroy" : RUN_CLEANUP

  void callback_destroy (GtkWidget *widget, gpointer data)
  {
    gtk_main_quit ();
  }
  ...
  GtkWidget *window;
  window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
  g_signal_connect (G_OBJECT (window), "destroy", G_CALLBACK (callback_destroy), NULL);
  ...

Widget이 제거될때 발생하는 Signal이다. 형식은 아래와 같다.
"void user_function (GtkWidget *widget, gpointer data)"

"button-press-event" : RUN_LAST

  gboolean callback_button_press (GtkWidget *widget, GdkEventButton *event, gpointer data)
  {
    printf ("버튼이 눌러졌습니다. \n");
    return FALSE;
  }

Widget을 마우스의 버튼으로 눌렀을때 발생하는 Signal이다. 형식은 아래와 같다.
"gboolean callback_button_press (GtkWidget *widget, GdkEventButton *event, gpointer data)"

반환값(return)에 대한 설명은 이미 전장이 "Signal & Callback & Handler"에서 충분히 했으므로 생략한다.

5. Event Structures

이벤트의 구조체

말그대로 이벤트의 구조체이다. 이벤트별로 이벤트의 특성에 맞는 구조체를 가진다. 이중 하나인 GdkEventButton을 예로 들어보자면 마우스의 버튼에 관련된 이벤트에 맞게 필요한 정보를 가지게 된다. 좌표, 눌러진 버튼의 번호, 타입등 다양한 정보를 가지고 있다. 이벤트 구조체는 종류가 다양하므로 API에서 확인해 보길 바란다.

Callback 함수로의 전달

Callback 함수의 형태를 보면 GdkEvent로 시작하는 구조체의 포인터를 파라미터로 전달하는것들이 있다. 해당 이벤트에 관련된 세부적인 정보를 Event Structure를 통해 넘겨주는 경우이다.

button-press-event의 경우 Callback 함수에서 어느 버튼이 눌러졌는지 알아야되는 경우에는 GdkEventButton->button을 확인하면 되는것이다.

6. Hi 똘츄~

Hi 똘츄 프로그램

파일명 'hi.c'로 아래의 "hi 똘츄~"를 만들어 보자.

  #include <gtk/gtk.h>
  // Window의 'destroy' Signal 발생시 호출될 Callback 함수
  void
  cb_window_destroy (GtkWidget *widget, gpointer data)
  {
    // Main Event Loop 종료 (프로그램의 종료)
    gtk_main_quit ();
  }
  // Button의 'button_press_event' Signal 발생시 호출될 Callback함수
  gboolean
  cb_button_press_event (GtkWidget *widget, GdkEventButton *event, gpointer data)
  {
    gtk_button_set_label (GTK_BUTTON (widget), "Button을 마우스로 눌렀습니다.");
    return FALSE;
    /*
     * 반환값은 FALSE가 기본이다. 이걸 TRUE로 반환한다면 default handler는 호출되지 않는다.
     * 강좌를 순서대로 쭉 봐왔다면 이것이 어떤 의미가 있는것인지 알것이다.
     * 만약 잘 모르겠다면 바로 전 장인 'Signal & Callback & Handler'를 다시 보라.
     * 잘차려진 밥상은 아니더라도 밥상 차린 사람의 성의를 생각해 천천히 꼭꼭 씹어먹기 바란다.
     */
  }
  int
  main (int argc, char *argv[])
  {
    // 대부분 Widget의 생성후 반환값 형식은 GtkWidget이므로 GtkWidget의 포인터로 변수 선언
    GtkWidget *window = NULL;
    GtkWidget *button = NULL;
    gtk_init (&argc, &argv);
    // 기본 Window(GtkWindow) 생성
    window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
    /*
    * Button(GtkButton) 생성 및 Label 변경
    * 생성과 label변경을 한번에 처리할 수도 있겠으나 이해를 돕기위해 나누었다.
    * 아래의 두가지는 'button = gtk_button_new_with_label ("Hi 똘츄~");' 로 합쳐도 된다.
    */
    button = gtk_button_new ();
    gtk_button_set_label (GTK_BUTTON (button), "Hi 똘츄~");
    // 기본 Window에 Button을 넣어준다.(GtkContainer에 대해서는 이미 설명하였으므로 생략한다)
    gtk_container_add (GTK_CONTAINER (window), button);
    // Signal 연결에 대해서는 전장인 "Signal & Callback & Handler"에서 설명하였다
    // 기본 Window의 X 버튼 클릭시 실행할 Callback 함수 연결
    g_signal_connect (G_OBJECT (window), "destroy",
                    G_CALLBACK (cb_window_destroy), NULL);
    // Button을 마우스 버튼으로 눌렀을때 실행할 Callback 함수 연결
    g_signal_connect (G_OBJECT (button), "button_press_event",
                    G_CALLBACK (cb_button_press_event), NULL);
    // Button과 Window를 화면에 표시한다.
    gtk_widget_show (button);
    gtk_widget_show (window);
    /*
    * Main Event Loop 생성 및 실행
    * gtk_main_quit 함수가 호출될때까지 다음 문장으로 진입할 수 없다.
    * 실질적으로 이때 화면상에 UI가 표시된다.
    */
    gtk_main ();
    // gtk_main_quit 함수가 호출되면 여기로 진입하게 된다.
    return 0;
  }

컴파일

  $gcc `pkg-config --cflags --libs gtk+-2.0` hi.c -o hi

컴파일에 관한 자세한 내용은 앞에서 설명했으니 생략한다.

실행

  $./hi

실행을 해보면 조그만 창에 버튼이 놓여져있고 버튼에는 "Hi 똘츄~"라는 글자가 보일것이다.

버튼을 눌러보면 Text가 변경되는것을 확인 할 수 있을 것이다. 바로 'button-press-event' Signal이 발생하여 g_signal_connect로 연결된 Callback 함수인 'cb_button_press_event' 함수가 호출되는것이다. Callback 함수 내에 gtk_button_set_label 함수를 호출하여, GtkButton의 label에 표시되는 Text를 변경하도록 구현하였다.

그럼 이제 창 상단 오른쪽에 창을 닫는 X버튼을 눌러보자. 프로그램은 종료되고 쉘 프롬프트로 돌아올것이다.

  $./hi  $(커서가 껌뻑껌뻑)

X를 눌렀는데 당연히 프로그램이 종료되어야 되는게 아니냐구 말하고 싶을것이다. 아니다. X를 누르면 창만 사라지도록 GTK에 구현되어져 있다. 프로그램 종료와는 별개이다.

X를 누르면 'delete_event(다음에설명)' Signal이 발생하고 그뒤에 'destroy' Signal이 발생한다. 그러므로  'g_signal_connect'로 'destroy' Signal에 연결한 Callback 함수인 'cb_window_destroy' 함수가 호출되고, 그 Callback 함수내에 'gtk_main_quit()' 함수를 호출하도록 구현하였기 때문에 프로그램 자체가 종료된다. 바로 'gtk_main()'으로 구동됐던 Main Event Loop가 중단된 것이다.

'destroy' Signal에 'cb_window_destroy'를 연결하지 않았다면 'gtk_main_quit()'가 호출되지 못했을 것이고, 그렇다면 창만 사라지고 프로그램은 종료되지 않은 상태가 지속된다.

이 내용에 관하여는 당신이 미남 미녀라면 충분히 이해가 되었을것이다. 그저 농일뿐이고(정말 그렇게 생각할지도 -0- 미남은 몰라도 일단 미녀는 뭐...), 지금까지의 과정을 무시하지 않았다면 낮설지 않은 내용이다. 만약 이해가 안가고 낮설다면 이전의 강좌들을 다시 보기 바란다 그래도 이해가 안될때는 내가 바보 천치이거나 내가 좀더 바보 천치이기 때문이다. (응? 무슨소리지?)

Callback 함수 반환값의 증명

프로그램을 약간 변경하자. 'cb_button_press_event' 함수 내의 'return FALSE;'를 'return TRUE;'로 고치고, 'cb_window_destroy' 함수 내의 'gtk_main_quit();'를 지우거나 주석 처리하자. 저장하고 다시 컴파일을 한뒤, 실행해보자.

  void
  cb_window_destroy (GtkWidget *widget, gpointer data)
  {
    gtk_main_quit ();
  }
  gboolean
  cb_button_press_event (GtkWidget *widget, GdkEventButton *event, gpointer data)
  {
    gtk_button_set_label (GTK_BUTTON (widget), "Button을 마우스로 눌렀습니다.");
    return FALSE;
  }

요거를 (설마 주석좀 뺐다고 못찾는 사람이 있지 않겠지 ~.~)

  void
  cb_window_destroy (GtkWidget *widget, gpointer data)
  {
  }
  gboolean
  cb_button_press_event (GtkWidget *widget, GdkEventButton *event, gpointer data)
  {
    gtk_button_set_label (GTK_BUTTON (widget), "Button을 마우스로 눌렀습니다.");
    return TRUE;
  }

요렇게

버튼을 눌러보면 재미있는 상황이 벌어질 것이다. Text는 바뀌지만 버튼의 모양이 눌러진 모양으로 바뀌지 않는다. 지금까지의 강좌를 이해했다면 너무나 당연하게 생각할 것이다.

앞서 설명했듯이 반환값이 TRUE이면 더이상 다른 handler들을 호출하지 않고 멈춘다.

'button-press-event'는 Signal Flag가 'G_SIGNAL_RUN_LAST'이고, 우리는 위에서 g_signal_connect로 Callback 함수를 연결하였다. 그러면 Handler 호출의 순서는 Callback 먼저, 그다음 default handler이다. 그런데 Callback 함수에서 반환값을 TRUE로 하여 다른 Handler가 더이상 호출되지 않게 되었다. Callback 이후에나 호출되는 default handler는 호출되지 않게 되는것이다. 'button-press-event' Signal의 default handler에는 버튼이 눌러진 모양으로 변경되도록 구현되어있다. 이걸 앞에서 못하게 막았으니 당연한것이다.

또 하나 창의 X 버튼을 눌러보자. 창은 사라진다 하지만 쉘 프롬프트로 돌아가지 않을것이다. 'destroy' Signal의 Callback 함수인 'cb_window_destroy' 함수내에서 'gtk_main_quit()' 함수를 없에버렸기 때문이다. 화면에는 아무것도 안보이지만 아직 프로세스가 종료되지 않은것이다. 'gtk_main()'함수가 호출된후 'gtk_main_quit()'함수가 호출되지 않았으니 Main Event Loop가 아직도 유효한 상태이다. 창도 Widget인데 Widget이 제거되었다고 프로세스가 종료되는것은 아니다. Widget은 프로그램에 한 요소일 뿐이다. 창이 뜨는것 자체가 프로세스가 구동되는것은 아니며 창이 사라지는것도 마찬가지로 프로세스의 종료를 의미하는것이 아니다. 그저 Widget이 하나 생성되었다가 제거되었을뿐이다. 그것만으로는 프로세스 본체에는 아무런 영향을 미치지 못한다.

'gtk_main_quit()'가 호출되어야만 비로소 'gtk_main()'으로 구동되었던 Main Event Loop가 종료되어, 실질적으로 'gtk_main()'함수가 끝나는것이며 'gtk_main()' 다음으로 진입하게 된다.

  $./hi

처음 실행했을때도 창의 X를 눌렀을때도 계속 이상태 -_-;

재미있는 실험을 딱하나만 더해보자 'button-press-event' Signal의 Callback 함수 연결을 g_signal_connect가 아닌 g_signal_connect_after로 바꾼뒤 컴파일 하여 실행해보자.

  g_signal_connect (G_OBJECT (button), "button_press_event",
                  G_CALLBACK (cb_button_press_event), NULL);

요거를

  g_signal_connect_after (G_OBJECT (button), "button_press_event",
                        G_CALLBACK (cb_button_press_event), NULL);

요렇게

실행해보면 아까 보다는 약간 재미없는 일이 벌어진다. 아니다 일이 벌어지지 않는다. 아무런 반응이 없다. 그저 버튼만 눌러질뿐 변해야할 Text가 변하지 않는것이다. 왜그럴까? 답은 간단하다. GtkButton에 구현된 'button-press-event' Signal의 default handler는 TRUE를 반환한다. g_signal_connect_after를 사용했으므로, default handler를 먼저 호출한뒤 Callback 함수를 호출한다. 그런데 GtkButton에 구현된 'button-press-event' Signal의 default handler가 TRUE를 반환한다 했으니 당연히 Callback 함수가 호출되지 않는것이다. 혹시라도 불만이 그득한가? GtkButton이 그렇게 구현되어 있는 것이니 본인에게 따지지 말아주길 바란다.

앞에서도 말했듯, 같은 Signal이라고 해서 default handler가 모두 같은 방식으로 작동하는것은 아니다. GtkWidget에서는 default handler를 구현하거나 지정하지 않고 각 Widget마다 특성에 맞게 구현하여 지정하는 경우도 있다. 'button-press-event'도 그와 같은 방식이다. 'button-press-event'라는 Signal을 등록하고 발생하도록 구현된것은 GtkWidget이지만 default handler는 각 Widget이 필요에 따라 각각 구현한다.


Comments