[GTK기초강좌-013] 내맘대로 Widget 배치 (GtkFixed)
이글의 원본은 http://www.gnome.or.kr -> 문서 -> 개발자 관련 자료 -> 그놈한국 강좌에 있습니다.
그놈한국에 연재(?)중이며 나중에 내용변경되면 맞추기 귀찮을거 같아서 그냥 쭉 긁어다 붙이고 모양은 안다듬습니다.
http://www.gnome.or.kr 에서 보시는게 더 편할 수도 있습니다.



1. GtkContainer

GtkContainer가 뭐라구요? 네 맞습니다! 바로 다른 Widget을 포함하는 Widget을 위한 기본 Class이다. 아이구 똑똑들 하셔라~ (재미 없나요? -_-;) 지긋지긋하게 닳고 닳도록 들었다. 지놈은 실제 구현된것도 별로 없지만 GUI를 위한 기본 모태가 되는 아주 중요한 놈이다. 이는 추상적인 클래스로서 혼자서는 별다른 일을 하지 못하지만 GtkBin, GtkTable, GtkFixed등이 GtkContainer를 상속받는다. 강좌가 진행되며 잘 기억이 안나는건 다시한번 찾아봐주는 센스를 기대해본다. 'Gtk에서의 화면구성과 GtkContainer'와 '여러개의 Widget 배치 (GtkTable)'에 있다. 

2. GtkTable 을 이용한 양식화된 배치

양식화된 배치

전장에서 GtkTable에 관해 말해보았다. 양식화된 형태로 먼저 Table을 구성하고 Widget을 원하는 Cell에 포함시켜 화면 표시를 위한 정보를 구성하는것이다. GtkBin을 상속받은 GtkWindow나 GtkButton등과 크게 다른점은 여러개의 Widget을 배치한다는것이다. 또 뒤에나올 GtkFixed와 다른점은 양식화된 형태로 Widget을 배치한다는 것이다. 이것은 또다른 큰 차이를 낳게 된다. 단순히 양식화된 형태에 widget을 넣는냐 내맘대로 마구 배치하느냐의 차이만 있는것은 아니라 상위 Container의 크기변경과도 관계가 있다. 이는 다음장으로 예정되어있는 "resize에서의 GtkTable과 GtkFixed"에서 자세히 설명해주도록 하겠다.

3. GtkFixed 를 이용하여 내맘대로 배치

후후후 드디어 화면 배치의 마지막 요소(내가 하게될 강좌에 한하여서이다.)인 GtkFixed에 대해서 알아볼 시간이 왔다.

여러개의 Widget을 포함

GtkFixed도 GtkContainer를 상속받는다. Widget을 포함 하는 Widget이라는 얘기이다. GtkTable처럼 여러개의 Widget을 포함 시킬 수 있다.

고정된 위치와 고정된 크기

GtkFixed의 가장 큰 특징은 내맘대로 배치할 수 있다는 것이다. 이는 실행중에도 마찬가지다 초기에 셋팅할때뿐만 아니라 실행중에도 내마음대로 위치를 바꾸고 크기를 조정할 수 있다. 내맘대로 배치 할 수 있다는것이 의미하는것이 무엇일까? 바로 Fixed라는 이름에서도 알 수 있듯이 GtkFixed내에 픽셀단위로 고정된 위치와 고정된 크기로 포함시킨다.

 

보이는 바와 같이 여러개의 Widget을 원하는 위치, 원하는 크기로 배치한다.

GtkFixed에 포함된 Widget은 저절로 변하지 않는다.

GtkTable은 자신의 크기 변하면 포함된 Widget들도 확대, 축소, 채우기의 설정에 따라 위치와 크기가 저절로 조정된다. 이와는 달리 GtkFixed는 원하는 위치와 원하는 크기로 포함시키고 GtkFixed의 크기가 변하더라도 포함된 Widget들은 위치와 크기가 그대로이다. 다만 이것은 GtkFixed 자신의 변화에 따라 포함된 Widget이 저절로 변하지 않는다는 것이지 GtkFixed내에 포함된 Widget들은 전혀 변경할 수 없다는 것이 아니다. 오히려 언제든 내가 원하는 위치에 내가 원하는 크기로 변경하여 배치시킬 수 있으며 이는 실행중에도 즉시 반영된다.

    

GtkTable의 경우 위의 그림과 같이 GtkTable의 크기가 늘어날경우 내부에 포함된 Widget들도 함께 늘어난다. 물론 무조건 늘어나는건 아니다. 확장, 축소, 채우기 등의 옵션에 의해 변한다. 아래의 그림을 보자.

 

GtkTable내에 포함된 Widget도 아무런 옵션을 주지 않은경우 크기가 변해도 위와같이 사이즈가 변하지 않는다.

혹시나 해서 한가지 말해줄것이 있다. 위의 경우처럼 GtkWindow 자체에 어떠한 Widget을 포함시킨경우 (여기서는 GtkTable) Window가 늘어나면 같이 늘어난다. 이것은 GtkBin의 기본속성이다. Container자체를 가득 채우게 되는것이다. 이는 옵션같은것으로 조정되는 성질의 것이 아니다. GtkBin을 상속받은 다른 Widget들에 다른 Widget을 포함시킬때도 모두 마찬가지이다.

  

GtkFixed의 크기가 늘어난 경우이다. 하지만 내부의 Widget들은 모두 그자리에 그 크기를 유지하고 있다. 이것은 옵션같은것으로 조정되는 성질의 것이 아니다.

위치의 이동과 크기의 변경

GtkFixed에 포함된 Widget은 GtkFixed의 크기가 변하더라도 저절로는 조정되지 않는다고 하였다. GtkFixed를 기준으로 위치와 크기가 고정되어 있다. 그럼 한번 셋팅하면 그만이라는 얘기인가? 설마~ 그렇다면 우리는 좌절할 수 밖에 없는 상황이 온다. (했던 얘기 또하고 했던 얘기 또하고 지겹겠지만 그냥 들었으면 한다 ㅡ.ㅡ) GtkFixed의 변화에 의해 포함된 Widget들이 저절로 조정되어지지 않을뿐 우리는 언제든 마음껏 Widget을의 위치와 크기를 변경할 수 있다. 역시나 내맘대로 이다.

Widget들은 서로간의 위치에 영향을 주지 않는다.

말 그대로 이다. Widget은 GtkFixed내에 어디에든 배치할 수 있다. Widget이 있는 영역에 다른 Widget을 배치한다면 그 위에 덮어져 자리하게 된다. GtkBin이나 GtkTable처럼 하나의 영역(GtkTable의 경우 Cell)에 하나의 Widget만 배치할 수 있는것이 아니다. GtkFixed내라면 어디든 상관없다. 심지어는 그 위치에 다른 Widget이 자리잡고 있어도 상관없다. 곂쳐지는 부분을 덮어쓸 뿐이다. GtkFixed는 커다란 하나의 영역만이 존재하고 그 내부에는 자유롭게 Widget을 포함시킬 수 있다.

 

'button1'과 'button2'가 일부 겹쳐져있다. 다른 Widget과 무관하게 어디든 위치를 정할 수 있다.

위치는 GtkFixed를 기준으로한 상대적인 위치

혹시라도 오해하는 사람이 있을까봐 설명을 해둔다. GtkFixed에서 말하는 위치란 GtkFixed를 기준으로한 상대적인 위치이다. 스크린 전체가 아니라 GtkFixed Widget을 기준으로 한다는것이다. 변경되지 않는다는 의미가 Window을 왼쪽으로 이동시켰는데 GtkFixed에 포함되어있던 Widget이 스크린을 기준으로 고자리에 고대로 덩그러니 있다는 얘기는 아니다 -_-; GtkFixed내에 포함된 Widget의 X위치가 0일때는 GtkFixed Widget내에서 0인것이지 화면 전체에서 0은 아니라는것이다. 고정이라고 하니 혹시라도 오해할 사람이 있을까봐 설명한다.

(넘 쓸대없나... 개발을 하려하는 사람중에 그런 사람이야 당연히 없겠지만 볼마우스 시절 컴퓨터를 처음사서 마우스를 허공에 번쩍 들어 움직이더니 컴퓨터가 고장났다는 사람도 있었다. 컴퓨터를 설치해주던 기사의 말을 들어보면 분명 자기가 처음 설치해줄때 쓰는것을 알려줬다고 하였다. 거침없는 오해 정말 무서운 것이다 --;)

GtkWidget* gtk_fixed_new (void);

GtkFixed를 생성하는 함수이다. 필요한 인자는 없다. 그렇다 GtkFixed는 복잡한듯 하지만 상당히 간편하고 유용한 Widget이다. GtkFixed는 단지 Widget을 포함시켜 배치하기 위한 판때기에 불과하다. GtkTable처럼 뭔가 칸이 쳐져있거나 옵션이 있는 것이 아니다. 단순하게 생각하면 된다. 그저 우리는 GtkFixed라는 판때기를 하나 만들고 판때기 위에 맘대로 가져다 붙이면 되는것이다. GtkBin이나 GtkTable처럼 한 영역에 (GtkTable의 경우에는 Cell)하나의 Widget만을 배치할 수 있는것이 아니고 마구 겹쳐넣는것도 된다. Widget을 포함시키는데 다른 Widget의 영향을 받지 않는다는것이다. 단 나중에 포함시킨 위젯이 기본적으로는 위로 온다.

void gtk_fixed_put (GtkFixed *fixed, GtkWidget *widget, gint x, gint y);

Widget을 GtkFixed에 포함시키는 함수이다. fixed는 GtkFixed 자기 자신이고, widget은 포함시킬 widget이다. x는 가로 좌표이고 y는 세로 좌표이다. 흔히 생각하는 x, y좌표를 생각하면 된다. 이 좌표는 GtkFixed를 기준으로 하는 상대적인 좌표이다. x가 0이면 GtkFixed의 맨 왼쪽 첫번째 픽셀인것이지 화면전체 또는 GUI 어플리케이션 전체에서의 위치는 아니다. GtkFixed에서의 위치이다. GtkFixed가 좌우 두개로 나누어진 GtkTable의 오른쪽에 포함되어 들어갈 수도 있는것이고, GtkWindow에 그냥 전체로 자리잡을 수도 있는것이다. GtkFixed이든 GtkTable이든 그외 무엇이든 소수의 일부 Widget을 제외하고는 그저 Widget일 뿐이고 Widget이 가지는 모든 특성을 그대로 가지고 있다. 상대적인 위치임을 설명하기 위해 말이 길었는데 GtkFixed를 기준으로한 상대적인 x, y 좌표라는것만 기억하고 이해하면 무리가 없다.

void gtk_fixed_move (GtkFixed *fixed, GtkWidget *widget, gint x, gint y);

한가지 기억해보자. 위에서 GtkFixed는 언제라도 위치를 변경할 수 있다 하였다. 이함수를 호출하여 GtkFixed내에 이미 포함되어진 Widget의 위치를 이동시키는것이다. 이는 실행중에라도 상관없으며 함수를 호출하게 되면 즉시 반영된다. 한가지 주의할것은 두번째 파라미터인 widget은 'gtk_fixed_put' 함수를 이용하여 이미 포함되어진 widget이어야한다.

GtkFixed내에 'button1'과 'button2'가 포함되어져 있다고 하자. 'button2'의 'clicked' Signal Callback 함수내에 gtk_fixed_move 함수를 호출하여 'widget' 파라미터에 'button1'을 넣어주면 'button1'이 GtkFixed를 기준으로 x와 y에 해당하는 위치로 이동하는것이다. 이는 예제에서 구현하여 확인해 주겠다.

  

gtk_fixed_move 함수를 호출하면 위와같이 된다. 'button2'를 주목

void gtk_widget_set_size_request (GtkWidget *widget, gint width, gint height);

GtkFixed와는 무관하게 GtkWidget에 해당하는 함수이다. GtkWidget의 최소 크기를 변경한다. 말그대로 최소크기이다 GtkTable처럼 포함되어진 Widget의 크기가 유동적인 경우 원래의 목적대로 최소 사이즈의 역할을 한다. GtkTable이 줄어들어도 이 사이즈 밑으로는 줄지 않는다. 그럼 GtkFixed에서는 어떨까? 최소사이즈를 지정하는 것이지만 GtkFixed의 경우 고정된 사이즈이다보니 이 함수를 호출하여 GtkWidget의 크기를 변경하게 되면 이 크기를 유지하게 된다. GtkWidget의 크기를 변경하는 방법은 여러가지가 있지만 이것이 제일 간단하니 일단 이것으로 설명을 하겠다.

이정도 되면 더이상 설명할 필요는 없겠으나 참고로 GTK에서는 화면의 구성요소인 Widget들은 GtkWidget을 상속받으므로 GtkWidget이 하는일은 모두 할 수 있다. GtkButton, GtkLabel, GtkTable, GtkEntry 등등 모든 Widget이 이 함수로 크기가 변한다.

    

gtk_widget_set_size_request 함수를 호출하면 위와같이 된다. 'button2'를 주목

간단히 정리해보자

gtk_fixed_new : GtkFixed 생성
gtk_fixed_put : GtkFixed내에 Widget을 포함시킨다.
gtk_fixed_move : GtkFixed내에 이미 포함되어진 Widget의 위치를 이동시킨다.
gtk_widget_set_size_request : GtkWidget의 최소크기를 변경한다. GtkFixed는 고정크기이므로 이크기가 유지된다.

4. GtkEntry

한줄의 텍스트를 입력할 수 있는 Widget

우리가 흔히 생각하는 입력 양식 Widget중 하나이다. 단 한줄만 입력할 수 있다. 해당 Widget에 포커스와 커서를 이동시키고 키보드를 쳐서 입력하거나 복사 붙이기 등을 사용한다.

GtkWidget* gtk_entry_new ();

GtkEntry를 생성하는 함수이다. 반환 타입은 GtkWidget의 포인터이다.

const char* gtk_entry_get_text (GtkEntry *entry);

GtkEntry에 입력되어 있는 Text를 반환하는 함수이다. 반환 타입은 char의 포인터이다. (문자열)

void gtk_entry_set_text (GtkEntry *entry, const char *text);

GtkEntry의 Text를 변경하는 함수이다. 특이한 사항은 없고, 'text' 파라미터에 변경할 Text의 char 포인터를 넣어준다. (이것도 문자열)

4. 사용할 Signal

"clicked" : RUN_FIRST

gboolean
cb_button1_clicked (GtkWidget *widget, gpointer data)
{
  GtkWidget *button = (GtkWidget *) data;
  gtk_widget_set_size_request (button, 50, -1);
}

GtkButton등에서 마우스를 클릭했을때 발생하는 Signal이다. Callback 함수의 형식은 아래와 같다.
void user_function (GtkWidget *widget, gpointer data);

"activate" : RUN_LAST

void
cb_entry_activate (GtkWidget *widget, gpointer data)
{
  GtkWidget *label = (GtkWidget *) data;
  const gchar *entry_text = gtk_entry_get_text (GTK_ENTRY (widget));
  gtk_label_set_text (GTK_LABEL (label), entry_text);
}

GtkEntry에서 키보드의 'Enter'키가 눌러졌을때 발생하는 Signal이다. Callback 함수의 형식은 아래와 같다.
"void user_function (GtkWidget *widget, gpointer data);

GtkEntry외에 다른곳에서도 사용하는지는 기억해내지 못했다. -_-;

5. 지겨운 "Hi 똘츄~"

오늘만들건 요 비슷한거

  #include <gtk/gtk.h>
  /*
   * GtkFixed를 Global로 선언한다.
   * Callback 함수에서 여러개의 Widget을 사용할 일이 있어서이다.
   * 이는 단순한 예제를 위하여 사용하는것이니 Global은 가급적 삼가토록 한다.
   * Callback에서 여러개의 Widget을 가져오는 방법은 후에 여러가지를 알려주겠다.
   */
  GtkWidget *fixed = NULL;
  // Window의 'destroy' Signal 발생시 호출될 Callback 함수
  void
  cb_window_destroy (GtkWidget *widget, gpointer data)
  {
    // Main Event Loop 종료 (프로그램의 종료)
    gtk_main_quit ();
  }
  // Button1의 'clicked' Signal 발생시 호출될 Callback함수
  gboolean
  cb_button1_clicked (GtkWidget *widget, gpointer data)
  {
    // g_signal_connect에서 넘어온 사용자 데이터(Button2)를 GtkWidget의 포인터로 타입 캐스팅
    GtkWidget *button = (GtkWidget *) data;
    /*
     * GtkWidget의 크기를 변경하는 함수
     * 넓이는 50으로 변경하고 높이는 변경하지 않는다.
     * 넓이와 높이의 값이 -1이면 변경하지 않는다
     */
    gtk_widget_set_size_request (button, 50, -1);
    /*
     * 반환값이 없다. default handler는 무조건 호출된다.
     */
  }
  // Button2의 'clicked' Signal 발생시 호출될 Callback함수
  gboolean
  cb_button2_clicked (GtkWidget *widget, gpointer data)
  {
    // g_signal_connect에서 넘어온 사용자 데이터(entry)를 GtkWidget의 포인터로 타입 캐스팅
    GtkWidget *entry = (GtkWidget *) data;
    /*
     * GtkFixed내에 포함된 Widget의 위치를 이동시키는 함수
     * fixed는 위에서 선언한 글로벌 변수이다.
     */
    gtk_fixed_move (GTK_FIXED (fixed), entry, 10, 50);
    // GtkEntry의 Text를 변경하는 함수
    gtk_entry_set_text (GTK_ENTRY (entry), "오!! Entry의 텍스트가 바꼈습니다.");
    /*
     * 반환값이 없다. default handler는 무조건 호출된다.
     */
  }
  // entry의 'activate' Signal 발생시 호출될 Callback함수
  void
  cb_entry_activate (GtkWidget *widget, gpointer data)
  {
    // g_signal_connect에서 넘어온 사용자 데이터(label)를 GtkWidget의 포인터로 타입 캐스팅
    GtkWidget *label = (GtkWidget *) data;
    // 자기 자신(entry)의 Text를 가져온다.
    const gchar *entry_text = gtk_entry_get_text (GTK_ENTRY (widget));
    // label의 Text를 entry의 Text롤 변경한다.
    gtk_label_set_text (GTK_LABEL (label), entry_text);
    /*
     * 반환값이 없다. default handler는 무조건 호출된다.
     */
  }
  int
  main (int argc, char *argv[])
  {
    // 대부분 Widget의 생성후 반환값 형식은 GtkWidget이므로 GtkWidget의 포인터로 변수 선언
    GtkWidget *window = NULL;
    GtkWidget *label = NULL;
    GtkWidget *entry = NULL;
    GtkWidget *button1 = NULL;
    GtkWidget *button2 = NULL;
    gtk_init (&argc, &argv);
    // 기본 Window(GtkWindow) 생성
    window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
    /*
     * Label(GtkLabel) 생성
     */
    label = gtk_label_new ("Hi 똘츄~");
    /*
     * label의 최소 크기를 변경한다.
     * GtkFixed내에 포함되는 Widget들은 왠만하면 지정해주는것이 좋다.
     * 높이는 기본크기 사용
     */
    gtk_widget_set_size_request(label, 100, -1);
    gtk_widget_show (label);
    /*
     * Entry(GtkEntry) 생성
     */
    entry = gtk_entry_new ();
    /*
     * entry의 최소 크기를 변경한다.
     * GtkFixed내에 포함되는 Widget들은 왠만하면 지정해주는것이 좋다.
     * 높이는 기본크기 사용
     */
    gtk_widget_set_size_request(entry, 150, -1);
    gtk_widget_show (entry);
    /*
     * Button1(GtkButton) 생성
     * 생성과 label변경을 한번에 처리할 수 있는 gtk_button_new_with_label 함수를 썼다.
     */
    button1 = gtk_button_new_with_label ("Button1");
    /*
     * Button1의 최소 크기를 변경한다.
     * GtkFixed내에 포함되는 Widget들은 왠만하면 지정해주는것이 좋다.
     * 높이는 기본크기 사용
     */
    gtk_widget_set_size_request(button1, 70, -1);
    gtk_widget_show (button1);
    /*
     * Button2(GtkButton) 생성
     * 생성과 label변경을 한번에 처리할 수 있는 gtk_button_new_with_label 함수를 썼다.
     */
    button2 = gtk_button_new_with_label ("Button2");
    /*
     * Button2의 최소 크기를 변경한다.
     * GtkFixed내에 포함되는 Widget들은 왠만하면 지정해주는것이 좋다.
     */
    gtk_widget_set_size_request(button2, 100, 100);
    gtk_widget_show (button2);
    /*
     * Fixed(GtkFixed) 생성
     * fixed변수는 최상단에 Global로 선언
     */
    fixed = gtk_fixed_new ();
    gtk_widget_show (fixed);
    /*
     * fixed의 최소 크기를 변경한다.
     * gtk_widget_set_size_request는 GtkFixed내에 포함된 Widget에만 국한된것이 아니다.
         어느 Widget이든 원하면 지정할 수 있다.
     */
    gtk_widget_set_size_request (fixed, 300, 300);
    // 기본 Window에 Fixed를 넣어준다.(GtkContainer에 대해서는 이미 설명하였으므로 생략한다)
    gtk_container_add (GTK_CONTAINER (window), fixed);
    /*
     * GtkFixed에 label을 child widget으로 포함시킨다.
     * 가로 위치 10 pixel, 세로 위치 10 pixel이다.
     */
    gtk_fixed_put (GTK_FIXED (fixed), label, 10, 10);
    /*
     * GtkFixed에 entry를 child widget으로 포함시킨다.
     * 가로 위치 120 pixel, 세로 위치 50 pixel이다.
     */
    gtk_fixed_put (GTK_FIXED (fixed), entry, 120, 50);
    /*
     * GtkFixed에 Button1을 child widget으로 포함시킨다.
     * 가로 위치 20 pixel, 세로 위치 150 pixel이다.
     */
    gtk_fixed_put (GTK_FIXED (fixed), button1, 20, 150);
   
    /*
     * GtkFixed에 Button2을 child widget으로 포함시킨다.
     * 가로 위치 70 pixel, 세로 위치 160 pixel이다.
     */
    gtk_fixed_put (GTK_FIXED (fixed), button2, 70, 160);
    // Signal 연결에 대해서는 전장인 "Signal & Callback & Handler"에서 설명하였다
    // 기본 Window의 X 버튼 클릭시 실행할 Callback 함수 연결
    g_signal_connect (G_OBJECT (window), "destroy",
                    G_CALLBACK (cb_window_destroy), NULL);
    /*
     * entry에서 Enter를 입력하였을때 실행할 Callback 함수 연결
     * 사용자 데이터로 마지막 파라미터에 label(포인터)를 넘긴다.
     */
    g_signal_connect (G_OBJECT (entry), "activate",
                    G_CALLBACK (cb_entry_activate), label);
    /*
     * Button1을 마우스 버튼으로 클릭했을때 실행할 Callback 함수 연결
     * 사용자 데이터로 마지막 파라미터에 button2(포인터)를 넘긴다.
     */
    g_signal_connect (G_OBJECT (button1), "clicked",
                    G_CALLBACK (cb_button1_clicked), (gpointer) button2);
    /*
     * Button2를 마우스 버튼으로 클릭했을때 실행할 Callback 함수 연결
     * 사용자 데이터로 마지막 파라미터에 entry(포인터)를 넘긴다.
     */
    g_signal_connect (G_OBJECT (button2), "clicked",
                    G_CALLBACK (cb_button2_clicked), (gpointer) entry);
    // Window를 화면에 표시한다.
    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` hi3.c -o hi3

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

실행

  $./hi3

이와 유사한 화면이 나타날것이다. 실제로는 조금 다르겠지만 (사실 많이 -_-;)

'button1'을 클릭해보자. 'button2'의 넓이가 줄어들것이다. 'clicked' Signal이 발생하여 g_signal_connect로 연결된 Callback 함수인 'cb_button1_clicked' 함수가 호출된다. Callback 함수 내에 gtk_widget_set_size_request 함수를 호출하여 'button2'의 크기를 변경하도록 구현하였다.

'button2'버튼을 클릭해보자. entry가 왼쪽으로 이동하고 entry의 내용이 변경되는 것을 볼 수 있다. 구구절절한 설명은 button1과 같고 Callback 함수내에 구현된 부분이 다르다. gtk_fixed_move 함수를 호출하여 'entry'의 위치를 이동시키고, gtk_entry_set_text 함수를 호출하여 'entry'의 Text를 변경한다.

'entry'를 마우스로 한번 클릭해보자. 포커스를 이동시키고 커서를 위치하기 위함이다. 이제 키보드에서 'Enter'키를 눌러보자. 'label'의 Text가 바뀌는것을 볼 수 있다. 'activate' Signal이 발생하여 g_signal_connect로 연결된 Callback 함수인 'cb_entry_activate' 함수가 호출된다. Callback 함수 내에 gtk_label_set_text 함수를 호출하여 'label'의 Text를 변경하도록 구현하였다.

GtkFixed는 그다지 어려운것이 아니다. 그냥 고정된 위치와 크기로 Widget을 포함 할 수 있는 Container라고 생각하면 된다.


강좌를 너무 오랜만에 등록하는 듯 하다. 한 2주? 그에대한 핑계를 좀 읊어(요거 철자가 맞는건가...) 보도록 하겠다 하겠다.
이전 강좌후에 바로 시작했으나 화면 캡쳐 몇개하고 파일을 Windows로 이전하는데 문제(?)가 발생하여 좌절하고 있었다.
사실 조금 귀찮은 방법을 동원하면 순식간에 끝났을 일이나 모두가 그렇듯 귀찮은 일은 행하는것보다 마음먹는것이 더 어렵다.(나만그런가 --;) 예를 들자면 엑스윈도우에서 파이어 폭스를 이용해 업로드를 한다던가...
워낙 우유부단하여 한번 마음을 정하기가 어려워서 그렇지 정하고나면 바꾸는걸 잘 안한다. 원래 생각은 리눅스에서 캡쳐한걸 파폭을 띄워 올릴까 다른 머신에 옮겨놓고 그곳의 MS Window에서 올릴까 하다가 MS Window로 선택 뭐 이런정도?!? 이때 문제가 발생했다. FTP를 띄우기도 귀찮고 삼바설정을 하기도 귀찮은것이다. 결국 다시 우왕좌왕하면서 어찌해야하나 고민하기 시작했다. 한가지를 결정하고 다니 또다른 선택의 기로에 섰다. 이런 젠장 -_-; 파이어폭스를 띄우면 드림위즈 파일에 저장해놔도 되고 gnome.or.kr에 직접 올려도 된다. 하지만 이를 마음먹는것은 더 귀찮고 어려운 일이다. 이글을 쓰고 있는 지금도 고민중이다. 먼저 글부터 써놔야겠다 ㅡ.ㅡ

"어린이들~ 스크린샷을 올리는 일은 매우 어려운 일이야"

맞다 고민이 또하나 있었다. 내 강좌가 너무 말만 많고 실질적인 도움이 되는건 별로 없는것이 아닌가 하는것이다. 그렇다고 겉으로 드러나는 부분에 대한것만 말해주기엔 아쉬운것이 너무많고 다 설명하자니 길고 거기에 쓸대없는 말까지 많은데 이부분(?)을 포기 하자니 이또한 물러설수 없다. 다른 강좌나 문서를 보며 고민도 해봤다.
내 강좌를 쭉 다시 읽어보다 결론 도출. 어느글엔가 이러한 뜻의 말을 적어놓았다. 안하는것 보단 낫다. 없는것 보단 낫다. 초심으로 돌아가고 과거를 돌아봐야하는겐가 앗 핫하하하핳핳핳핳~ 아잉~ 샤방~~ 후덜덜덜

뭐 대략 이렇다 일단 각종 뻘스러운 고민가 선택의 기로 시작 우유부단중은 여전한지 확인
do {
  온갖뻘짓;
  우유부단중은 여전한가?;
} while (우유부단중);

가장 아름다운 해답은 성격개선의 의지와 노력 -0-;
(다음부터는 그나마 약간이라도 여흥을 더해주기 위해 나의 샤방한 이미지와는 전혀 맞지 않는 수년간 축척된 잠이 좀 깨는 사진을 가끔 올려주도록 하겠다.)

나와는 전혀 상관없는 이미지

시범적으로 한컷
위 사진은 절대 본인의 사진이 아님!!!.

아 오늘도 말이 참 많았다. 좀 구질구질 하구나. 뭐 난 원래 구질구질 하다 앗핫핫핳핳핳핳하하하하~~~~ -0-

질문은 이곳의 질답 게시판이나 이메일로 보내주시기 바란다

참고로 본인의 이메일은 스팸으로 그득해서 카드 고지서외에는 거의 눈에 띄지 않지만
미모의 여성이 보낸 편지는 본능적으로 클릭해 낸다.

너는 잘난것이 아니다.
남과는 서로 다른것을 알고 있을 뿐이다.

by 소하 | 2007/06/04 13:52 | GTK 따라하시든가 | 트랙백 | 핑백(1) | 덧글(3)
[GTK기초강좌-012] 여러개의 Widget 배치 (GtkTable)
이글의 원본은 http://www.gnome.or.kr -> 문서 -> 개발자 관련 자료 -> 그놈한국 강좌에 있습니다.
그놈한국에 연재(?)중이며 나중에 내용변경되면 맞추기 귀찮을거 같아서 그냥 쭉 긁어다 붙이고 모양은 안다듬습니다.
http://www.gnome.or.kr 에서 보시는게 더 편할 수도 있습니다.



1. GtkContainer

GtkContainer는 다른 Widget을 포함하는 Widget을 위한 기본 Class라고 설명하였다. 전장의 "Hi 똘츄~"를 보자 GtkWindow를 만들고 그 GtkWindow 내에 Button을 하나 배치하였다. 이때 GtkWindow 내에 버튼을 포함시키기 위해서 gtk_container_add 함수를 호출하였으며, GtkWindow를 GTK_CONTAINER매크를 사용해 GtkContainer로 타입캐스팅 하였다. GtkWindow를 GtkContainer로 타입캐스팅 할 수 있다는것은 GtkContainer로 부터 상속 되어졌기 대문에 가능한것이다.

여기서 의문점이 하나 생겼으면 참 좋겠다. -_-; gtk_container_add로는 GtkContainer에 하나밖에 배치할 수 없는것 같다. 맞다. 앞에서도 이미 설명하였다. 그럼 뭔가 설마 프로그램에 Widget하나만 배치해서 써야하나? (참으로 유치하다 젠장 --;) 설마 그럴리가 있겠나. GtkTable, GtkFixed등을 이용하면 다채로운 형태의 화면 구성이 가능하다 말했었다.

(심하게 감기가 든채로 약먹고 침대에 엎드려 글을 쓰고 있다. 정신이 헤롱헤롱한가보다. 글빨이 안나온다 ㅡ.ㅡ 뭐 원래 글빨이 없긴 했지만 ~.~)

2. GtkTable

드디어 베일에 쌓여져있던 GtkTable에 대해 알아볼때가 닥쳐와 버렸다. GtkFixed는 조금더 베일에 쌓여있게 냅두자.

양식화된 틀 속에 Widget들을 포함시킨다.

GtkTable은 Word 프로그램의 표와같이 양식화된 형태로 Widget들을 포함시켜 화면에 표시하는것이다. colspan, rowspan등 셀 병합도 지원한다. 앞장의 화면구성과 GtkContainer를 설명하며 화면을 구성하는것은 정보의 구성이라 하였다. GtkTable은 GtkBin처럼 GtkContainer의 기본적 기능을 사용한다고 보긴 어렵다. 다양한 형태로 구성되다 보니 그를 충족시키기위해 내부적으로는 list가 존재하고 그곳에 GtkTable에 포함될 Widget들의 화면 표시 정보가 담겨있다. gtk_container_add는 백날 해봐야 첫번째 Cell에만 들어가도록 GtkTable에 구현되어져 있다. (왠지 구색 맞추기 같다. 아무래도 GtkContainer를 상속받다보니...)

 
(정말 대단하지 않은가? 그간 귀찮음에 허덕이며 모든것을 말로 때워오던 본인이 캡쳐씩이나 해버린것이 아닌가.!!!)

Glade에서 Window에 2x2 GtkTable을 하나 찍으면 위와같은 모양이 나온다. 검정색 테두리 내부가 GtkTable 부분이다. 프로그램을 구동하였을때 실제 작동시 저렇게 보인다는것은 아니다. 다만 개발 툴에서는 영역구분과 개발자가 얻을수 있는 정보가 확실해야하다보니 구분을 주고 있는것이다. Column과 Row의 구분이 확실하니 좋지 않은가.

마치 워드 프로그램에서 표를 그려놓은것만 같다.

GtkWidget* gtk_table_new (guint rows, guint columns, gboolean homogeneous);

GtkTable을 생성하는 함수이다. rows는 행이고 columns는 열이다. homogeneous는 포함된 Widget들의 사이즈를 균일하게 유지하기 위해 사용한다.

void  gtk_table_attach (GtkTable *table, GtkWidget *child,
                  guint left_attach, guint right_attach,
                  guint top_attach, guint bottom_attach,
                  GtkAttachOptions xoptions, GtkAttachOptions yoptions,
                  guint xpadding, guint ypadding);

gtk_table_new 함수 호출로 생성되어진 GtkTable의 Cell 속에 Widget을 포함시키는 함수이다.

table gtk_table_new로 반환되어진 GtkWidget의 포인터를 타입 캐스팅 해서 넣으면 된다. GTK_TABLE(table)과  같이 말이다.
child GtkTable 내부에 포함되어질 Widget의 포인터 이다.
left_attach, right_attach GtkTable내부에서 child widget이 위치할 가로(열, X) 좌표이다. left는 시작 좌표이고 right는 끝 좌표이다.
top_attach, bottom_attach GtkTable내부에서 child widget이 위치할 세로(행, Y) 좌표이다. top은 시작 좌표이고 bottom은 끝 좌표이다.
xoptions, yoptions Table의 크기에 맞추어 Cell의 크기를 변경할지와, Cell에 child widget을 가득 채울지를 결정. x는 가로이고 y는 세로이다. 
xpadding, ypadding cell 내부의 여백 값.

이런 굉장히 많은듯 싶다. 하지만 몇가지만 알아두고 주의를 기울이면 된다.

4개의 _attach는 위치(좌표)를 가르키며 Cell 병합 기능도 한다.

..._attach는 파라미터 이름만봐도 왼쪽, 오른쪽, 위, 아래라는것을 알 수 있다. 굉장히 간단해 보이고, 어려울것 같지 않은데 뭘 설명을 다 해줄려그러냐고 할런지도 모른다. 평소에는 제대로 불친절하던 인간이 말이다. ㅡ.ㅡ 절!대!오!산! 내용을 모르면 헷갈리기 쉽상이다. 다음 코드를 보자.

가장 왼쪽 첫번째 줄의 Cell에 Widget을 포함시키는 예제이다.
gtk_table_attach (GTK_TABLE (table1), label1, 0, 1, 0, 1, ...);
뭔가 좀 이상하지 않나? 0, 1이라니 Cell의 좌표라고 봤을때 0, 0, 0, 0이 되야하는거 아닌가? 그래 본인도 처음엔 그렇게 생각했다. 최!강!똘!츄! 아 하~하하하하. API에서 함수만 덜렁보고 아싸 하고 말았으니 당연한 일이다. 해당값은 Cell자체의 X, Y 좌표가 아니라 Cell 좌우상하 Side 바로 가생이를 기준으로 해야했던것이다. 맨 왼쪽 첫번째 줄 Cell 하나에도 _attach 4개가 존재한다. 좌 = 0, 우 = 1, 상 = 0, 하 = 1. Widget이 자리할 Cell을 기준으로 좌, 우, 상, 하의 값이 되는것이다. 아래를 보면 쉽게 이해가 된다. Tutorial에서 살짝 떠왔다.
 0          1          2
0+----------+----------+
 |          |          |
1+----------+----------+
 |          |          |
2+----------+----------+

(삐뚤어도 그냥 참고들 보시라~~  ~.~ 정 우울하면 메모장이나 고정폭 글꼴을 사용해 붙여넣고 보면 잘 보인다.
어허허허 이런 이 기사 띄우기도전에 fender옹이 고정폭 달아줘버리셨다.)

다들 이해가 됐으리라 보고 이제 병합에 대해 말하겠다. 간단하다 위에서 말한 것을 기준으로 right_attach와 bottom_attach를 늘려주면 되는것이다. 너무 간단해서 놀래 버렸는가? -0- 모든것이 이렇듯 기본 원리나 작동방식만 알고나면 상상외로 쉽게 풀려나간다. 특히나 GTK는 누구나 쉽게 생각되어질 수 있는 범위에서 함수명을 정하고 파라미터의 용도를 정의한듯하다. 제작자의 아름다운 배려가 놀랍지 않나? (아무생각없었으면 어쩌지 -_-; 농담이다 절대 그래선 이런 훌륭한 툴킷이 탄생할 수 없다.)
gtk_table_attach (GTK_TABLE(table), label, 0, 2, 0, 1, ...) 이렇게 right_attach 파라미터를 2로 바꾸면 첫번째 줄에서 왼쪽부터 2개의 Cell을 차지하게 된다. 아래의 그림을 보면 'button1'과 같은 형태이다.

 

Table의 크기에 유연한 Widget

이부분이 약간 까다로울수 있으나 기본 작동 방식 각각을 먼저 뜯어서 생각한뒤 종합적으로 보면 이또한 매우 간단하다. (어찌 하는 소리가 항상 같은가 라고 생각할지도 모르겠다 던질 수 있는 떡밥이 이정도 뿐인데 어쩌랴. 그냥 좀 참고 해보자 -_-;)

xoptions, yoptions? X와 Y의 옵션이라는건 알겠고 뒤에 S가 붙었으니 옵션들(-_-;)이란것도 알겠는데 그럼 무슨 옵션인가? 위에서 말했듯이 크기에 관련된 옵션이다. Table 크기에 맞추어 Cell의 크기를 변경할것인지(확대, 축소), Cell에 child widget의 크기를 가득 채워 맞출것인지를 결정한다. Table의 크기가 변할때도 함께 반응한다. 그럼 먼저 GtkAttachOptions에 대해 알아보자.

  typedef enum
  {
    GTK_EXPAND = 1 << 0,
    GTK_SHRINK = 1 << 1,
    GTK_FILL   = 1 << 2
  } GtkAttachOptions;

자 척 봐도 bit단위로 연산될것이라는 것을 알 수 있다. 왜 옵션들인지 이제 알것이다. 파라미터의 값을줄때 '|'로 쭉 나열해서 주면 다 더해져서 값이 넘어가 bit단위로 비교되어 처리된다.
gtk_table_attach (GTK_TABLE (table), label, 0, 1, 1, 1, GTK_EXPAND | GTK_FILL, ...) 뭐 대략 이런식이다. Table 크기에 따라 Cell의 크기도 함께 변하게 할지에 대해 정의하는 옵션이라 하였다. 또 Cell의 크기에 맞게 child widget을 채울 수도 있다. 아래의 표를 보자 (오늘 gnome.or.kr 글쓰기에 있는 표를 처음 써봤다 재미들렸다 잇힝~)

GTK_EXPAND Table의 크기에 맞추어 Cell이 확장되어 늘어난다. 하지만 widget이 Cell에 가득차게 늘어나는것은 아니다.
GTK_SHRINK

Table의 크기에 맞추어 Cell이 줄어든다. 하지만 widget이 Cell에 가득차게 줄어드는 것은 아니다.
줄어드는것은 최소사이즈를 child widget에서 정할 수 있다. child widget에서 사이즈를 정해놓으면 그 이상은 줄어들지 않는다.

GTK_FILL child widgdet을 Cell에 가득 채운다. GTK_EXPAND옵션을 주어 확장된다 하더라도 child widget의 사이즈가 늘어나는것은 아니다. 이 옵션을 주어야만 Cell에 가득찬다.


아래의 그림을 보자 'button2'는 xoptions에 'GTK_EXPAND | GTK_FILL'을 주고 yoptions에는 'GTK_EXPAND'만 준 경우이다. 그다음 'button3'은 xoptions와 yoptions 둘다 'GTK_EXPAND | GTK_FILL'을 주었다. 차이점을 알겠는가? x는 같으니 신경쓰지 말고 y만 보자. 'button2'의 yoptions에서는 GTK_EXPAND는 주었으므로 Cell은 늘어났다 하지만 GTK_FILL을 주지 않아 child widget인 'button2'는 Cell의 중앙에 자리한채 원래 크기를 유지하고 있다. 그에 반해 'button3'은 GTK_EXPAND와 GTK_FILL을 함께 주어 Cell도 늘어나고 child widget인 'button3'도 늘어났다.

하나더 참고할 내용이 있다. Cell의 크기는 같은 열과 행에 함께 적용 된다. 작은쪽이 큰쪽에 맞춰진다. 'label2'의 높이가 이것저것 옵션까지 다 따져도 '100' 이라고 하자. 같은 행에 있는 'button3'의 높이가 옵션등에 의해 '150' 이 필요하다면 'label2'의 높이도 '150' 에 맞춰진다는 것이다. 결국 열과 행의 크기가 같은 열 같은 행에 있다는 다른 Cell의 크기에 영향을 받는다. 다시한번 말하지만 작은쪽이 큰쪽에 맞춰진다. 하나하나 제멋대로 움직이지는 않는다.

GtkTable또한 화면을 표시하는 부분에 있어서 앞에서 설명한 바와같이 내부에 값들로 이루어진 정보가 구성된다고 생각해라. 결국 화면을 표시할때 해당 정보들을 활용하는 것이다.

3. Signal Callback 함수의 사용자 데이타

g_signal_connect 함수에서 마지막 인자는 사용자 데이터이다. "Hi 똘츄~"에서는 그냥 NULL을 넣어 보냈다. 하지만 이번에는 GtkLabel을 넘겨 GtkButton의 Signal에서 GtkLabel을 제어하는것을 보게 될 것이다. 그냥 global 변수로 해도 되겠지만 global 변수는 왠만하면 지양해주는 센스가 필요하다. Callback에 사용자 데이터를 넘길 수 있는 좋은 방법이 있으니 알아보자.

이번에 만들 화면의 기본형태이다. 'button2'를 마우스로 누르면 'label1' 의 내용이 변하고, 'button3'에 마우스를 클릭(GTK에서는 마우스를 눌렀다가 땔때 'clicked' Signal이 발생한다 이때 마우스의 포인터는 해당 Widget내에 있어야한다.)할때 'label2'의 내용이 변하게 할것이다.

그럼 이러한 작동에 대해 Callback 함수를 만들어 g_signal_connect로 연결해야할텐데 'label1'과 'label2'는 어떻게 넘겨줄것인가? 간단하다. global 변수를 만들어 선언하면 된다. 그렇게 할까? 아니다. 아름다운 방법이 있다. 바로 Callback 함수의 사용자 데이타로 'label1'과 'label2'의 Widget 포인터를 넘겨주고 Callback에서 받아서 쓰는것이다.

gboolean callback_func (GtkWidget *widget, GdkEventButton *event, gpointer data)
이것이 'button-press-event' Signal Callback 함수의 형태이다. 마지막에 보자 gpointer data라는것이 있다. 마지막 파라미터이 바로 이 data가 사용자 정의 data이다. GTK에서 기본적으로는 사용하지 않는 데이타이다. 사용자가 g_signal_connect 함수를 호출할때 원하는것을 넘겨줄수 있다. gpointer는 void * 즉 형식이 없는 포인터이다. 어떠한 타입의 포인터도 넘겨줄 수 있다는 것이다. int *, char *, GtkWidget * 등등 포인터형이라면 원하는건 무엇이든 넘길 수 있다.

  gboolean
  callback_func (GtkWidget *widget, GdkEventButton *event, gpointer data)
  {
    GtkWidget *label = (GtkWidget *) data;
    ...
  }


이와 같이 gpointer로 넘어온 사용자 데이타를 타입캐스팅 하여 사용하면 된다.

g_signal_connect도 한번 보자.
g_signal_connect (G_OBJECT (button), "button_press_event", G_CALLBACK (cb_button_press_event), NULL);
전장에 있던 내용이다. 여기서 마지막에 NULL로 넘긴것 바로 이 마지막 파라미터에 사용자 데이터를 넣는다. 이 사용자 데이터는 Signal을 관리하는 공간에 함께 저장되어있다가, Signal이 발생하여 Callback 함수를 호출할때 함께 전달한다.

4.또한번 질러보자 "Hi 똘츄~ 2"

"Hi 똘츄~ 2"를 만들어 보기 위한 기본적인 것들은 지금까지 지나오면 알아봤다. GtkLabel도 전장에서 사용은 하지 않았지만 함께 챙겨보았다. 그러니 상세한 내용은 생략 -0-. GtkLabel은 Gtk에서 텍스트를 표시하는 가장 기본적이면서도 대표적인 Widget이다. GtkButton은 Button중의 기본이며 여러 다른 Button들이 GtkButton을 상속받아 구현한다. GtkTable은 표 처럼 양식화된 형태로 Widget을 넣을 수 있다. 그리고 한가 더 알아본것이 Signal의 Callback 함수에 g_signal_connect 함수를 호출할때 파라미터로 사용자 데이터를 넘길 수 있다는것도 알아봤다.

"Hi 똘츄~"와 달라진건 거의 없다 GtkLabel과 GtkTable을 쓴다는것과 Callback 함수에 사용자 데이터를 넘긴다는것외에는 동일하므로 어렵지 않을것이다 천천히 훓어보고, 직접 짜보고, 실행해 보고, 만져 보고, 느껴 보자. 잘 안되면 다시 보고 반복해보자. 쉽게 얻은것은 쉽게 잃기 마련이다. 지금까지의 강좌를 두편이내로 만들어 쉽게 알수 있고 또 프로그램 제작까지 가능하게 만들어 줄 수도 있다. 하지만 아는것보다는 이해하는것이 스스로를 간지나게 만들어준다. (응? 간지? 제길 역시 끝이 안좋구나 선천적인 진지 결핍증인가 췟!)

Hi 똘츄~ 2 프로그램

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

  #include <gtk/gtk.h>
  // Window의 'destroy' Signal 발생시 호출될 Callback 함수
  void
  cb_window_destroy (GtkWidget *widget, gpointer data)
  {
    // Main Event Loop 종료 (프로그램의 종료)
    gtk_main_quit ();
  }
  // Button1의 'button_press_event' Signal 발생시 호출될 Callback함수
  gboolean
  cb_button1_press_event (GtkWidget *widget, GdkEventButton *event, gpointer data)
  {
    // g_signal_connect에서 넘어온 사용자 데이터(label1)를 GtkWidget의 포인터로 타입 캐스팅
    GtkWidget *label = (GtkWidget *) data;
    // GtkLabe의 Text를 변경하는 함수
    gtk_label_set_text(GTK_LABEL(label), "Button1을 마우스로 눌렀습니다.");
    return FALSE;
    /*
     * 반환값은 FALSE가 기본이다. TRUE이면 default handler가 호출되지 않는다.
     */
  }
  // Button2의 'button_release_event' Signal 발생시 호출될 Callback함수
  gboolean
  cb_button2_release_event (GtkWidget *widget, GdkEventButton *event, gpointer data)
  {
    // g_signal_connect에서 넘어온 사용자 데이터(label2)를 GtkWidget의 포인터로 타입 캐스팅
    GtkWidget *label = (GtkWidget *) data;
    // GtkLabe의 Text를 변경하는 함수
    gtk_label_set_text(GTK_LABEL(label), "Button2를 마우스로 눌렀다가 땠습니다.");
    return FALSE;
    /*
     * 반환값은 FALSE가 기본이다. TRUE이면 default handler가 호출되지 않는다.
     */
  }
  int
  main (int argc, char *argv[])
  {
    // 대부분 Widget의 생성후 반환값 형식은 GtkWidget이므로 GtkWidget의 포인터로 변수 선언
    GtkWidget *window = NULL;
    GtkWidget *table = NULL;
    GtkWidget *label1 = NULL;
    GtkWidget *label2 = NULL;
    GtkWidget *button1 = NULL;
    GtkWidget *button2 = NULL;
    gtk_init (&argc, &argv);
    // 기본 Window(GtkWindow) 생성
    window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
    /*
     * Label1(GtkLabel) 생성
     */
    label1 = gtk_label_new ("Hi 똘츄~ 1");
    gtk_widget_show (label1);
    /*
     * Label2(GtkLabel) 생성
     */
    label2 = gtk_label_new ("Hi 똘츄~ 2");
    gtk_widget_show (label2);
    /*
     * Button1(GtkButton) 생성
     * 생성과 label변경을 한번에 처리할 수 있는 gtk_button_new_with_label 함수를 썼다.
     */
    button1 = gtk_button_new_with_label ("Button1");
    gtk_widget_show (button1);
    /*
     * Button2(GtkButton) 생성
     * 생성과 label변경을 한번에 처리할 수 있는 gtk_button_new_with_label 함수를 썼다.
     */
    button2 = gtk_button_new_with_label ("Button2");
    gtk_widget_show (button2);
    /*
     * Table 생성
     * 가로 2 x 세로 2 의 Cell을 가진 테이블을 생성한다.
     * Cell 크기의 균일화는 하지 않는다. (마지막 FALSE)
     */
    table = gtk_table_new (2, 2, FALSE);
    gtk_widget_show (table);
    // 기본 Window에 Table을 넣어준다.(GtkContainer에 대해서는 이미 설명하였으므로 생략한다)
    gtk_container_add (GTK_CONTAINER (window), table);
    /*
     * 맨 윗줄 첫번째 Cell에 label1을 child widget으로 넣는다.
     * 가로(xoptions)의 크기는 테이블의 크기에 맞춰 확장하고, Cell에 맞게 child widget을 채운다.
     * 세로(yoptions)의 크기는 테이블의 크기에 맞춰 확장한다.
     * 안 여백은 xpadding = 0, ypadding = 0으로 여백을 주지 않는다.
     */
    gtk_table_attach (GTK_TABLE (table), label1, 0, 1, 0, 1,
                (GtkAttachOptions) (GTK_EXPAND | GTK_FILL),
                (GtkAttachOptions) (GTK_EXPAND),
                0, 0);
    /*
     * 맨 윗줄 두번째 Cell에 button1을 child widget으로 넣는다.
     * 가로(xoptions)의 크기는 테이블의 크기에 맞춰 확장하고, Cell에 맞게 child widget을 채운다.
     * 세로(yoptions)의 크기는 테이블의 크기에 맞춰 확장한다.
     * 안 여백은 xpadding = 0, ypadding = 0으로 여백을 주지 않는다.
     */
    gtk_table_attach (GTK_TABLE (table), button1, 1, 2, 0, 1,
                (GtkAttachOptions) (GTK_EXPAND | GTK_FILL),
                (GtkAttachOptions) (GTK_EXPAND),
                0, 0);
    /*
     * 두번째줄 첫번째 Cell에 label2를 child widget으로 넣는다.
     * 가로(xoptions)의 크기는 테이블의 크기에 맞춰 확장하고, Cell에 맞게 child widget을 채운다.
     * 세로(yoptions)의 크기는 테이블의 크기에 맞춰 확장한다.
     * 안 여백은 xpadding = 0, ypadding = 0으로 여백을 주지 않는다.
     */
    gtk_table_attach (GTK_TABLE (table), label2, 0, 1, 1, 2,
                (GtkAttachOptions) (GTK_EXPAND | GTK_FILL),
                (GtkAttachOptions) (GTK_EXPAND),
                0, 0);
    /*
     * 두번째줄 두번째 Cell에 button2를 child widget으로 넣는다.
     * 가로(xoptions)의 크기는 테이블의 크기에 맞춰 확장하고, Cell에 맞게 child widget을 채운다.
     * 세로(yoptions)의 크기는 테이블의 크기에 맞춰 확장하고, Cell에 맞게 child widget을 채운다.
     * 안 여백은 xpadding = 0, ypadding = 0으로 여백을 주지 않는다.
     */
    gtk_table_attach (GTK_TABLE (table), button2, 1, 2, 1, 2,
                (GtkAttachOptions) (GTK_EXPAND | GTK_FILL),
                (GtkAttachOptions) (GTK_EXPAND | GTK_FILL),
                0, 0);
    // Signal 연결에 대해서는 전장인 "Signal & Callback & Handler"에서 설명하였다
    // 기본 Window의 X 버튼 클릭시 실행할 Callback 함수 연결
    g_signal_connect (G_OBJECT (window), "destroy",
                    G_CALLBACK (cb_window_destroy), NULL);
    /*
     * Button1을 마우스 버튼으로 눌렀을때 실행할 Callback 함수 연결
     * 사용자 데이터로 마지막 파라미터에 label1(포인터)을 넘긴다.
     */
    g_signal_connect (G_OBJECT (button1), "button_press_event",
                    G_CALLBACK (cb_button1_press_event), (gpointer) label1);
    /*
     * Button2를 마우스 버튼으로 눌렀다 땠을때 실행할 Callback 함수 연결
     * 사용자 데이터로 마지막 파라미터에 label2(포인터)를 넘긴다.
     */
    g_signal_connect (G_OBJECT (button2), "button_release_event",
                    G_CALLBACK (cb_button2_release_event), (gpointer) label2);
    // Window를 화면에 표시한다.
    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` hi2.c -o hi2

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

실행

  $./hi2

이와같은 유사한 화면이 나타날 것이다.

'button1'버튼을 마우스의 버튼으로 누르고 때지 말아보아라. 바로 'button-press-event' Signal이 발생하여 g_signal_connect로 연결된 Callback 함수인 'cb_button1_press_event' 함수가 호출되는것이다. Callback 함수 내에 gtk_label_set_text 함수를 호출하여, GtkLabel의 Text를 변경하도록 구현하였다. 확인한뒤 마우스의 버튼에서 손을 때도 좋다.

'button2'버튼을 마우스로 누르고 때지 말아보아라. 아무런 변화가 없을것이다. 'button2'버튼의 Signal은 'button-release-event' Signal에 연결하였다. 마우스 버튼을 눌렀다가 땟을때 발생한다. g_signal_connect로 연결된 Callback 함수인 'cb_button2_release_event' 함수가 호출되는것이다. Callback 함수 내에 gtk_label_set_text 함수를 호출하여, GtkLabel의 Text를 변경하도록 구현하였다.

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

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

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

Callback 함수 사용자 데이터의 사용

전장의 "Hi 똘츄~"와는 다른점이 있다. 바로 GtkButton의 Callback 함수에서 전혀 다른 Widget인 GtkLabel을 제어한다는것이다. Widget뿐만 아니라 어떠한 포인터라도 전달할 수 있다.

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

  g_signal_connect (G_OBJECT (button1), "button_press_event",
G_CALLBACK (cb_button1_press_event), (gpointer) label1);

소스내에 보면 위와같은 내용이 있다. 눈여겨 보아야할것은 마지막 파라미터 '(gpointer) label1' 이다. 설명했던바와 같이 g_signal_connect의 마지막 인자는 사용자 데이터를 형식이 없는 포인터로 전달한다. gpointer 타입이 형식이 없는 포인터이다 (void *)와 같은 효과를 가진다. 'label1'은 GtkWidget이므로 gpointer로 타입캐스팅하여 전달한다.

  gboolean
cb_button1_press_event (GtkWidget *widget, GdkEventButton *event, gpointer data)
{
GtkWidget *label = (GtkWidget *) data;
gtk_label_set_text(GTK_LABEL(label), "Button1을 마우스로 눌렀습니다.");
return FALSE;
}

Callback함수를 보자. 'GtkWidget *label = (GtkWidget *) data' 마지막 파라미터인 'data'를 GtkWidget의 포인터로 타입캐스팅 하였다. g_signal_connect에서 전달된 사용자 데이타를 쓰기 편하게 GtkWidget타입으로 바꾸었다. 이제 main함수내에서 생성했던 GtkLabel인 'label1'을 제어할수있다. 그렇게 타입캐스팅한 label의 Text를 바꾸기 위해 gtk_label_set_text 함수를 호출하였다.

Callback 함수의 사용자 데이터를 어렵게 생각할 필요는 없다. 그저 포인터일 뿐이다. g_signal_connect 함수 호출시 마지막 파라미터로 넘기고 싶은 데이타는 무엇이든지 포인터로 넘길 수 있는것이다. 그냥 함수 하나 만들었는데 그 함수의 파라미터중 포인터 타입이 있고 그 함수를 호출할때 그 파라미터로 포인터를 하나 전달했다 생각하면 된다. 기본 원리는 똑같고 다만 Callback 함수는 Signal이 발생했을때 내부적으로 호출한다는것이다. 사용자 데이터도 당연히 이때 넘기는것이다.

 


 

화면구성과 GtkContainer 강좌와 이 강좌 두개 쓰는데 무려 일주일이 소요됐다.
극심한 몸살감기에 머리속도 몸도 말이 아니게 지쳐있다.
쉬었다 다시 시작할까 하는 생각도 했으나 쉬는것은 스스로가 허락하지 못하는듯하다.
뭐든지 하고 있어야 그나마 마음의 평화가 찾아온다.
쉴줄 모르는것도 병인듯 하다.

질문은 이곳의 질답 게시판이나 이메일로 보내주시기 바란다

참고로 본인의 이메일은 스팸으로 그득해서 카드 고지서외에는 거의 눈에 띄지 않지만
미모의 여성이 보낸 편지는 본능적으로 클릭해 낸다.

너는 잘난것이 아니다.
남과는 서로 다른것을 알고 있을 뿐이다.

by 소하 | 2007/06/04 13:50 | GTK 따라하시든가 | 트랙백 | 덧글(0)
[GTK기초강좌-011] 기다리 고기다리던 (재밌다 그래줘요 ㅡ.ㅡ) "Hi 똘츄~"를 만들어보자
이글의 원본은 http://www.gnome.or.kr -> 문서 -> 개발자 관련 자료 -> 그놈한국 강좌에 있습니다.
그놈한국에 연재(?)중이며 나중에 내용변경되면 맞추기 귀찮을거 같아서 그냥 쭉 긁어다 붙이고 모양은 안다듬습니다.
http://www.gnome.or.kr 에서 보시는게 더 편할 수도 있습니다.



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 &lt;gtk/gtk.h&gt;
  // 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 &amp; Callback &amp; Handler'를 다시 보라.
     * 잘차려진 밥상은 아니더라도 밥상 차린 사람의 성의를 생각해 천천히 꼭꼭 씹어먹기 바란다.
     */
  }
  int
  main (int argc, char *argv[])
  {
    // 대부분 Widget의 생성후 반환값 형식은 GtkWidget이므로 GtkWidget의 포인터로 변수 선언
    GtkWidget *window = NULL;
    GtkWidget *button = NULL;
    gtk_init (&amp;argc, &amp;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 &amp; Callback &amp; 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이 필요에 따라 각각 구현한다.


강좌를 써나가며 느끼는것인데 사람들이 나를 보면 만두만 사줄까 두렵다.
"sohalee는 말이 너무 많아요"
거기다 썰렁하기까지 하니 이거 원 --;
이젠 머리도 나쁜게 아닌가 하는 생각마저 든다. ~.~

질문은 이곳의 질답 게시판이나 이메일로 보내주시기 바란다

참고로 본인의 이메일은 스팸으로 그득해서 카드 고지서외에는 거의 눈에 띄지 않지만
미모의 여성이 보낸 편지는 본능적으로 클릭해 낸다.

너는 잘난것이 아니다.
남과는 서로 다른것을 알고 있을 뿐이다.

by 소하 | 2007/06/04 13:37 | GTK 따라하시든가 | 트랙백 | 덧글(0)
[GTK기초강좌-010] Signal & Callback & Handler
이글의 원본은 http://www.gnome.or.kr -> 문서 -> 개발자 관련 자료 -> 그놈한국 강좌에 있습니다.
그놈한국에 연재(?)중이며 나중에 내용변경되면 맞추기 귀찮을거 같아서 그냥 쭉 긁어다 붙이고 모양은 안다듬습니다.
http://www.gnome.or.kr 에서 보시는게 더 편할 수도 있습니다.



이 기사를 쓰기 위해 루나씨를 이틀간 너무 귀찮게했다. 이게 다 delete_event 때문이다 --; 나의 잠시간의 헷갈림으로 인해 안드로메다에 다녀왔다. 너무 많이 알려하지 말길 바란다.

1. Signal에 Callback 함수 연결

'g_signal_connect'와 'g_signal_connect_after'는 Signal에 사용자 정의 handler인 Callback 함수를 연결해준다.

이미 Event와 Signal, 그리고 Main Event Loop에 대해 간략히 알아보았다. 그때 Event가 생겨 Signal이 발생하면 Callback 함수를 호출한다 하였는데 바로 이 Callback 함수를 원하는 Signal에 연결하는것이다. 연결된 Callback 함수는 해당 Signal이 발생할때마다 호출된다.

Signal의 종류에 따라 Callback 함수의 형식은 이미 정해져있다. 해당 Callback 함수를 형식에 맞추어 작성하고 g_signal_connect  함수를 호출할때 Callback 함수의 포인터를 파라미터로 넘겨준다.

g_signal_connect(instance, detailed_signal, c_handler, data)

  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);
...

"destroy"는 Widget이 제거될때 발생하는 Signal이다. 형식은 위에서 보는 바와 같이 "void user_function (GtkWidget *widget, gpointer data)"이다.

default handler

Signal이 발생할때 우리가 g_signal_connect로연결한 Callback 함수만 호출하고 끝나는것이 아니다. 사용자 정의 handler라고 할 수 있는 Callback 함수를 호출하기도 하고 기본적으로 처리해야할 것들이 정의된 default handler를 호출하기도 한다. Signal이 발생했을때 기본적으로 처리해야할 작업이 default handler에 정의되어있는 것이다.

앞서 우리는 Signal이 발생했을때 호출하도록 g_signal_connect로 Callback 함수를 연결할 수 있는것을 보았다. 이것과는 별개로 Signal이 발생했을때 Widget이 해야할 가장 기본적인 기능들이 있다. GtkButton의 경우 마우스로 버튼을 누르게 되면 버튼이 눌러진 모양이 되어야한다.

Widget의 형태에 따라 기본적으로 해야할 작업이 정의 되어 있고, Signal 발생시 Callback 함수와 함께 호출되는것이 바로 default handler이다.

default handler는 같은 Signal이라고 해서 다 똑같은 것은 아니다. GtkWidget에서는 Signal을 등록만 하고 default handler는 지정하지 않는 경우가 있다. 'button-press-event'가 그러하다. Widget을 마우스의 버튼으로 눌렀을때 GtkButton이 기본적으로 해야할일과 GtkEntry가 기본적으로 해야할일은 다르다. GtkButton은 버튼이 눌러진 모양으로 변해야할 것이며, GtkEntry는... (너무 길다 ㅡ.ㅡ 'gtkentry.c'의 'gtk_entry_button_press' 함수를 참고하기 바란다.) 에헴 뭐 그렇다 --; 중요한것은 Signal은 같지만 default handler는 Widget마다 특성에 따라 다르게 구현될 수 있다는 것이다.

마지막으로 default handler도 반환값이 있으면 Signal 발생에 의한 handler 제어에 영향을 준다. 'button-press-event'같은 경우 default handler는 TRUE를 반환하여, 'g_signal_connect_after'로 Callback 함수를 연결해 놓으면 호출되지 않는다. 자세한 내용은 아래의 내용들을 모두 보고나면 이해가 될것이다.

g_signal_connect와 g_signal_connect_after의 차이점

Signal에는 Flag가 있다. 이는 GTK 내부에 Signal마다 미리 정의해 놓았다. Signal의 Flag에 따른 handler 호출 절차를 알아두게 되면 어플리케이션의 섬세한 제어를 할 수 있다. Signal마다 어떤 Flag를 사용하는지는 API에 보면 각 Widget설명의 Signal 항목에 잘 나와있다. 그럼 아래를 한번 보자.

  typedef enum  {
G_SIGNAL_RUN_FIRST = 1 << 0,
G_SIGNAL_RUN_LAST = 1 << 1,
G_SIGNAL_RUN_CLEANUP = 1 << 2,
G_SIGNAL_NO_RECURSE = 1 << 3,
G_SIGNAL_DETAILED = 1 << 4,
G_SIGNAL_ACTION = 1 << 5,
G_SIGNAL_NO_HOOKS = 1 << 6
} GSignalFlags;

이것이 Signal Flag의 종류이다. 'G_SIGNAL_RUN_FIRST'와 'G_SIGNAL_RUN_LAST'만 설명하겠다.
'G_SIGNAL_RUN_FIRST'로 지정된 Signal은 Callback 함수보다 default handler를 먼저 호출한다. default handler 호출, 그다음 Callback 함수 호출이다. 반대로 'G_SIGNAL_RUN_LAST'는 Callback 함수가 호출되고 난 후 default handler가 호출된다. 단 여기에 예외사항이 있다. 바로 g_signal_connect_after이다. g_signal_connect_after로 연결된 Callback 함수는 FIRST이던 LAST이던 default handler 함수가 호출된 후에 Callback 함수가 호출된다.

이제 두개의 차이를 어느정도 이해 할 것이다. g_signal_connect로 연결된 Callback 함수는 FIRST냐 LAST에 따라 default handler보다 먼저 호출 될 수도 나중에 호출 될 수도 있지만 g_signal_connect_after는 default handler보다 나중에 호출된다.

다음은 GObject API에 있는 내용이다. (Signal은 GObject 소속이다.) 

RUN_FIRST: if the G_SIGNAL_RUN_FIRST flag was used during signal registration and if there exist a class_closure for this signal, the class_closure is invoked. Jump to EMISSION_HOOK state.

EMISSION_HOOK: if any emission hook was added to the signal, they are invoked from first to last added. Accumulate return values and jump to HANDLER_RUN_FIRST state.

HANDLER_RUN_FIRST: if any closure were connected with the g_signal_connect family of functions, and if they are not blocked (with the g_signal_handler_block family of functions) they are run here, from first to last connected. Jump to RUN_LAST state.

RUN_LAST: if the G_SIGNAL_RUN_LAST flag was set during registration and if a class_closure was set, it is invoked here. Jump to HANDLER_RUN_LAST state.

HANDLER_RUN_LAST: if any closure were connected with the g_signal_connect_after family of functions, if they were not invoked during HANDLER_RUN_FIRST and if they are not blocked, they are run here, from first to last connected. Jump to RUN_CLEANUP state.

RUN_CLEANUP: if the G_SIGNAL_RUN_CLEANUP flag was set during registration and if a class_closure was set, it is invoked here. Signal emission is completed here.

Callback 함수의 반환값

Callback 함수의 형식 중에 반환값이 있는것이 있다. 그중 자주 사용하는 반환값의 형식은 gboolean 이고 이것은 handler 호출의 과정을 중단시킬 수 있다.

위에서 말했듯 Signal이 발생했을때 handler가 하나만 호출되는것이 아니다. 여러개의 handler가 조건에 따라 순차적으로 호출되는데 이때 handler의 반환값이 TRUE냐 FALSE냐에 따라 계속 진행할 것인지, 멈출것인지가 결정된다. Callback 함수의 예제들을 보면 대부분 FALSE인 것을 알 수 있다. 그렇다 FALSE일 경우에는 계속해서 진행하고 TRUE면 진행을 멈추어 그 이후에 호출해야할 handler는 호출되지 않는다.

예를 들어보자면 'button_press_event' Signal의 Callback 함수에 반환값을 TRUE로 주게 되면, 그 이후에 호출되어야할 default handler가 호출되지 않아, 버튼이 눌러진 모양으로 변하지 않는다. 다음에 나올 "Hi 똘츄~"예제를 이용해 확인해 주도록 하겠다.

위에서도 말했지만 default handler도 반환값이 TRUE인 경우에는 그 다음 handler가 호출되지 않는다.


강좌를 써나가며 느끼는것인데 사람들이 나를 보면 만두만 사줄까 두렵다.
"sohalee는 말이 너무 많아요"
거기다 썰렁하기까지 하니 이거 원 --;
이젠 머리도 나쁜게 아닌가 하는 생각마저 든다. ~.~

질문은 이곳의 질답 게시판이나 이메일로 보내주시기 바란다

참고로 본인의 이메일은 스팸으로 그득해서 카드 고지서외에는 거의 눈에 띄지 않지만
미모의 여성이 보낸 편지는 본능적으로 클릭해 낸다.

너는 잘난것이 아니다.
남과는 서로 다른것을 알고 있을 뿐이다.

by 소하 | 2007/06/04 13:31 | GTK 따라하시든가 | 트랙백 | 덧글(0)
[GTK기초강좌-009] GTK 어플리케이션 컴파일
이글의 원본은 http://www.gnome.or.kr -> 문서 -> 개발자 관련 자료 -> 그놈한국 강좌에 있습니다.
그놈한국에 연재(?)중이며 나중에 내용변경되면 맞추기 귀찮을거 같아서 그냥 쭉 긁어다 붙이고 모양은 안다듬습니다.
http://www.gnome.or.kr 에서 보시는게 더 편할 수도 있습니다.



1. gcc 

GNU C 컴파일러이다. 리눅스 뿐만 아니라 솔라리스나  MS  윈도우즈에서도 사용 가능하다. 기본적으로 libc는 glibc를 사용한다.

  $gcc test.c -o test

가장 기본적인 형식이다. '-o'는 output 즉 결과물인 실행파일이다.

다음과 같은 프로그램을 만들어 실행해 보자 후에 pkg-config를 설명할때도 사용할것이니 한번 만들어보자. 파일명은 test.c로 작성하도록 하자.

  int main (int argc, char *argv[]) {
int i = 0; // i = 1부터 시작해야한다. 1부터 명령행에 넣은 인자이다.
for (i = 1; i < argc; i++) {
printf(" %s", argv[i]);
}
printf("\n");
return 0;
}

그럼 이것을 위에 설명했던 방법으로 컴파일한뒤 실행해보자.

  $gcc test.c -o test  $./test hi  hi

아무문제없이 컴파일되었다면 위와같은 결과가 나온다.

2. pkg-config

'man pkg-config' 를 해보면 NAME의 항목에 "Return metainformation about installed libraries"와 같이 설명되어있다. 간단하지만 가장 잘 설명해 놓은 말이다. 현재 내 시스템에 설치되어있는 라이브러리의 META정보를 반환한다.

pkg-config는 우리가 컴파일시에 주로 하던 '-I/usr/include...'와 '-lglib-2.0'등과 같은 라이브러리의 인크루딩 플래그와 링크 플래그를 미리 METAFILE에 정의해 하나의 패키지로 사용한다. gcc로 컴파일시에 인크루딩 플래그와 링크 플래그를 옵션으로 일일이 직접넣지않고 패키지명만으로도 원하는 플래그들을 자동으로 완성하여 넣어준다.

아래와 같이 실행해보자 보통 glib은 모두들 설치되어있으므로 왠만하면 될것이다.

  $pkg-config --cflags --libs glib-2.0  -I/usr/include/glib-2.0 -I/usr/lib/glib-2.0.include -lglib-2.0

나의 시스템에서는 위와같이 나온다. 설명한 바와 같이 뭔가 반환했다. 그럼 저것은 무엇일까? 바로 gcc 컴파일시에 사용할 수 있는 옵션들이다. pkg-config 명령이 인크루딩 플래그와 링크 플래그를 반환했음을 알 수 있다.

이제 대충 감이 올것이다. pkg-config는 미리 METAFILE을 만들어 패키지화 시켜놓고 컴파일시을 편리하게 해 주는 것이다. 그럼 저 내용을 어떻게 컴파일시 gcc 명령에 포함하는지는 조금 더 뒤에 알아보도록 한다.

METAFILE

pkg의 약자는 무엇일까? 너무나 쉬운가? 패키지이다. pkg-config는 라이브러리의 인크루딩 플래그와 링크 플래그를 미리 METAFILE에 정의해놓고 컴파일시에 사용한다는건 이미 설명한 바로부터 알 수 있다. 시스템에 아무리 라이브러리가 깔려있다해도 METAFILE이 없으면 pkg-config는 인식할 수 없다. METAFILE을 만듬으로서 패키지화 시키게 되는것이다.

본인은 우분투를 사용하는데 우분투에는 '/usr/lib/pkgconfig'밑에 METAFILE들이 존재한다. 확장자는 '.pc'이며 패키지명 + 확장자이다. 만약 어떤 라이브러리에 대해 'sohalee.pc'라고 METAFILE을 생성한다면 패키지명은 'sohalee'가 되는것이다. 위에서 보았던 'glib-2.0.pc'라는 파일도 있을것이다. 내용은 아래와 같다.

  prefix=/usr
exec_prefix=${prefix}
libdir=${exec_prefix}/lib
includedir=${prefix/include
...
Libs: -L${libdir} -lglib-2.0
Cflags: -I${includedir}/glib-2.0 -I${libdir}/glib02.0/include

리눅스 머신에서 내용을 보며 윈도우에서 글을 작성중이라 다 치고 앉아있기 너무 힘들어 중간은 생략했으나 각자들 파일을 열어보기 바란다 ㅡ.ㅡ

보면 'Libs'와 'Cflags'의 내용이 실제 컴파일시에 옵션으로 들어간다는것을 알 수 있을것이다. 자 그럼 여기서 또 하나알수 있는것이 위에서는 생략하고 넘어온 pkg-config 명령의 옵션에 대한 설명이다. '--cflags'는 'Cflags'에 정의된 내용을, '--libs'는 'Libs'에 정의된 내용을 반환값에 포함시키도록 한다.

'glib-2.0' 패키지에는 없는 내용인데 중요한것이 있다. 'gtk+-2.0.pc' 정도 되는 파일을 열어보게 되면 'Requires'라는게 있다 저곳에는 pkg-config의 다른 패키지명을 넣는것이다. 너무 많을땐 'Libs'와 'Cflags'에 일일이 쳐넣기 귀찮기도 하고 또 만약 함께 사용하는 다른 라이브러리의 구성이 바뀌기라도 한다면 관련된 METAFILE들을 모두 다 바꿀수는 없는 노릇이다. 그래서 각각의 패키지를 METAFILE로 구성해놓고 서로서로 불러서 마치 자신의 METAFILE에 직접 지정한것과 같은 효과를 보게 된다.

pkg-config를 이용한 컴파일

이렇게 장황하게 설명했는데 그럼 대체 컴파일은 어떻게 하란 말인가? 아래를 보자

  $gcc `pkg-config --cflags --libs gtk+-2.0` hello.c -o hello
*) 주의사항 pkg-config 옵션을 감싸고 있는 따옴표는 쌍따옴표와 함께있는 외따옴표가 아니라
숫자 1 왼쪽에 있는 물결 밑의 따옴표이다.

위에서 'test.c'를 컴파일 할때와 무엇이 달라졌는가? 바로 컴파일 명령에 pkg-config에 관련된 내용이 추가 되었다. 위에서도 설명했듯이 pkg-config는 인크루딩 플래그와 링크 플래그를 반환한다. 실제로 GTK 어플리케이션을 컴파일 하기 위해선 엄청난 옵션이 줄줄이 붙어야하지만 pkg-config가 'gtk+-2.0.pc'를 읽어와 필요한 플래그들을 완성한뒤 반환해 주게되는것이다.

다음을 직접 실행하여 결과를 보도록 하자.

  $ pkg-config --cflags gtk+-2.0
-I/usr/include/gtk-2.0 -I/usr/lib/gtk-2.0/include -I/usr/include/glib-2.0
-I/usr/lib/glib-2.0/include -I/usr/include/pango-1.0 -I/usr/X11R6/include
-I/usr/include/freetype2 -I/usr/include/atk-1.0
$ pkg-config --libs gtk+-2.0
-L/usr/lib -L/usr/X11R6/lib -lgtk-x11-2.0 -lgdk-x11-2.0 -lXi
-lgdk_pixbuf-2.0 -lm -lpangox -lpangoxft -lXft -lXrender -lXext -lX11
-lfreetype -lpango -latk -lgobject-2.0 -lgmodule-2.0 -ldl -lglib-2.0

사실 우리는 저걸 컴파일 명령행에 다 넣어주었어야하는것이다. pkg-config를 이용하면 실제로 'gcc 위의결과내용들 hello.c -o hello'와 같은 컴파일 명령을 내린것이 되는것이다.

여러개의 패키지를 사용하여 컴파일 하려면 아래와 와같이 패키지명을 나열해주면된다.
`pkg-config --cflags --libs gtk+-2.0 libgnomecups-1.0`

만약 GTK 개발용 라이브러리도 설치되어있는데 컴파일이 안된다면 pkg-config 패키지 설정파일중 GTK와 관련된 파일명이 무엇인지 확인해 보아야 할 것이다.

오는 길이 이리도 멀었는데 결과가 너무 허무한가? 딱 저한줄만 보면 허무하겠지만 저 한줄을 만들어내기위해 pkg-config를 만드느라 날밤깠을 개발자에게 경의를 좀 표해주자. 별거아니라 생각할 수도 있겠으나 그대는 했는가?
(제길 재미없으니 농담은 그만 집어 치우자 -_-; 이렇게 간단한걸 왜이리도 어렵고 길게 설명했지라고 생각한다면 그대는 더이상 이 강좌를 볼 필요가 없다. 앞으로도 이럴것이기 때문이다. 여기까지 쭉 강좌가 이어지며 왜 그래야하는가를 누누히 설명하였으므로 생략하겠다. 그나마 pkg-config는 이 강좌에서 권외라고 생각되기도 하고 본인의 역량을 뛰어넘는 부분이라 아는것만 줄여서 짧게한것이다.)

사용법은 의외로 간단하지만 저렇게 만들기 위해 pkg-config라는것이 있어야하고 pkg-config에서 사용할 수 있게 METAFILE을 만들어 패키지화 시켜놓아야한다. pkg-config에 대한 배려가 되지 않은 라이브러리를 사용할때라도 매번 컴파일 옵션을 일일이 쳐 넣는것이 아니라 METAFILE을 만들어 사용한다면 매우 편리하게 사용할 수 있을것이다. 본인 역시 예전에 gtkmozembed였나 poppler였나 여튼 무언가를 사용하기 위해 만들어 썼던 기억이 있다.

헉 이런 젠장

강좌를 보며 흡족해 하고 있는데 움찔해버렸다. 위에서 'test.c'를 만든 이유를 까마득히 잊고 있었다. 머리에 '무안단물'이라도 뿌려줘야하는건가. 그래 그건 pkg-config가 하는것을 확실하게 보여주기 위해서였다. 한번 해보자 --;

  $./test `pkg-config --cflags --libs gtk+-2.0`
-I/usr/include/gtk-2.0 -I/usr/lib/gtk+-2.0/include... (늠흐 길다 -0-)

위에서 'test.c'는 명령행에 인자로 넘어오는 내용들을 모두 출력하게끔 제작되어졌다 그렇다 `pkg-config --cflags --libs gtk+-2.0` 바로 이것으로 인해 실제 명령행에는 위와같은 내용이 포함되어진 것이다.

만약 아직 gtk+-2.0이 아직 없다면 glib-2.0이나 METAFILE이 존재하는 다른 패키지명을 넣어서 결과를 확인해보라


강좌를 써나가며 느끼는것인데 사람들이 나를 보면 만두만 사줄까 두렵다.
"sohalee는 말이 너무 많아요"
거기다 썰렁하기까지 하니 이거 원 --;
이젠 머리도 나쁜게 아닌가 하는 생각마저 든다. ~.~

질문은 이곳의 질답 게시판이나 이메일로 보내주시기 바란다

참고로 본인의 이메일은 스팸으로 그득해서 카드 고지서외에는 거의 눈에 띄지 않지만
미모의 여성이 보낸 편지는 본능적으로 클릭해 낸다.

너는 잘난것이 아니다.
남과는 서로 다른것을 알고 있을 뿐이다.

by 소하 | 2007/06/04 13:24 | GTK 따라하시든가 | 트랙백 | 덧글(0)


< 이전페이지 다음페이지 >