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

여러개의 Widget 배치 (GtkTable)

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을 포함시키는 함수이다.

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

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

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_EXPANDTable의 크기에 맞추어 Cell이 확장되어 늘어난다. 하지만 widget이 Cell에 가득차게 늘어나는것은 아니다.
GTK_SHRINK

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

GTK_FILLchild 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이 발생했을때 내부적으로 호출한다는것이다. 사용자 데이터도 당연히 이때 넘기는것이다.

 

Comments