다음 이전 차례

20. 자신만의 widget 만들기

20.1 개요

비록 GTK 배포판에 포함되어 있는 수많은 widget들이 대부분의 기본적인 요구사항을 충족시켜 주지만, 언젠가 스스로 새로운 widget을 만들어야 할 때가 올 것이다. GTK가 widget의 상속을 광범위하게 이용하고 또 이 만들어져 있는 widget들이 여러분의 요구에 근접한 것들이기 때문에, 유용하고 새로운 widget을 만드는 것도 단지 몇 줄의 코드로써 가능할 수도 있다. 그러나 새로운 widget을 만드는 작업을 시작하기 전에, 먼저 누군가 그것을 이미 만들어 놓지 않았는지 체크하자. 이렇게 해서 노력의 중복을 막고 또 GTK widget의 갯수를 최소한으로 유지할 수 있다. 이것은 서로 다른 어플들 사이에서 코드와 인터페이스 모두의 통일성을 유지하는 것이다. 이렇게 하는 한가지 방법으로, 만약 여러분이 자신만의 widget을 완성하게 되면, 그것을 다른 사람들 모두 이용할 수 있게 세계에 널리 알리자. 이렇게 할 수 있는 가장 좋은 장소는 아마 gtk-list일 것이다.

예제들의 완전한 소스 코드는 당신이 이 문서를 구한 곳이나 또는 다음 장소에서 구할 수 있다.

http://www.msc.cornell.edu/~otaylor/gtk-gimp/tutorial

20.2 Widget의 구조

새로운 widget을 만드는 데 있어서, GTK object들이 어떻게 동작하는지 이해 하는 것이 중요할 것이다. 이번 섹션은 단지 짧막한 개요만을 소개하고자 한다. 자세한 것은 참고 문서를 보라.

GTK widget들은 객체지향적 방법을 지향한다. 그러나, 그들은 표준 C로 쓰여진다. 이것은 현재 수준의 C++ 컴파일러를 쓰는 것보다 이식성과 안정성을 크게 향상시킨다. 하지만 이것은 widget 작성자가 몇몇 세부적인 곳에 주의를 기울여야만 하는 것을 의미한다. 한 클래스의 widget들(예를 들자면 모든 Button widget들)의 instance들 모두에 공통적인 정보는 class structure에 저장되어 있다. 이 class의 시그널들은 함수에 대한 포인터, 즉 callback 함수로 처리 되고, C++에서의 virtual 함수처럼 하나의 복사본만이 존재해 준다. 상속을 지원하기 위해서, class structure의 첫번째 필드는 parent class structure의 copy가 되어야 한다. GtkButton이라는 class structure의 선언은 이렇게 된다.

struct _GtkButtonClass
{
        GtkContainerClass parent_class;

        void (* pressed)  (GtkButton *button);
        void (* released) (GtkButton *button);
        void (* clicked)  (GtkButton *button);
        void (* enter)    (GtkButton *button);
        void (* leave)    (GtkButton *button);
};

버튼이 하나의 컨테이너로 다루어진다면(예를들어, resize되었을 때), 그것의 class structure는 GtkContainerClass로 캐스트될 수 있고, 또 시그널들을 다루기 위해 관련된 필드들이 쓰이게 된다.

하나의 instance basis를 기반으로 한 각각의 widget들을 위한 구조체도 있다. 이 구조체는 어떤 widget의 각각의 instance를 위한 서로 다른 정보를 가지고 있다. 우리는 이 구조체를 object structure라고 부를 것이다. 버튼 클래스를 예로 든다면 이렇게 된다.

struct _GtkButton
{
        GtkContainer container;

        GtkWidget *child;

        guint in_button : 1;
        guint button_down : 1;
};

Class structure에서와 마찬가지로 첫번째 필드는 parent class의 object structure임을 주의하라. 그래서 이 구조체는 필요할 경우 parent class의 object로 캐스트될 수도 있는 것이다.

20.3 Composite(합성,혼성) widget 만들기

소개

우리가 만들고자 하는 어떤 widget은 단지 다른 GTK widget들의 집합체일 뿐일 수도 있다. 이런 widget은 꼭 새로운 것으로 만들어져야 할 것은 아니지만, 재사용성을 위해 사용자 인터페이스적인 요소들을 패키징하는 데 있어 편리한 방식을 제공한다. 표준 GTK 배포판에 있는 FileSelection과 ColorSelection widget이 예가 될 것이다.

우리가 여기서 만들려는 예제는 Tictactoe widget으로, 토글버튼의 3x3 배열 에서 어느 한 행 또는 열 또는 대각성분 모두가 눌러지게 되면 시그널을 내보낸 다.

Parent class를 고르기

전형적으로 composite widget의 parent class는 그 composite widget의 모든 멤버들을 가지고 있는 컨테이너 클래스이다. 예를들어, FileSelection widget의 parent class는 Dialog 클래스이다. 우리 버튼들은 table 안에 늘어서 있을 것 이므로, parent class로 GtkTable 클래스가 되는 것이 자연스럽다. 불행히도, 이것은 동작하지 않는다. 한 widget을 만드는 것은 두 함수에 의한 것으로 나뉜다. WIDGETNAME_new() 함수를 사용자가 부르고, 그리고 WIDGETNAME_init()함수는 _new()함수의 인자로 주어진 widget들에 대한 기본적인 초기화 작업을 한다. Descendent widget들은 단지 그들의 parent widget의 _init()함수만 부르게 된다. 그러나 이 작업 분할은 테이블에 대해서는 잘 동작하지 않는다. 테이블의 경우에는 만들어질 때 행과 열의 갯수를 알 필요성이 있기 때문이다. 그렇지 않으면 우리는 이 Tictactoe widget에 있는 gtk_table_new()의 대부분의 기능들을 중복해서 이용하게 될 것이다. 그러므로, 우리는 대신 그것을 GtkVBox 로부터 이끌어내어, 우리의 테이블을 VBox 내부로 붙여줄 것이다.

헤더 파일

각각의 widget class는 public 함수와 구조체 및 객체의 선언들이 있는 헤더 파일을 가지고 있다. 여기서 간단히 살펴보자. 중복 정의를 피하기 위해서, 우리는 헤더파일 전체를 이렇게 둘러싼다.

#ifndef __TICTACTOE_H__
#define __TICTACTOE_H__
.
.
.
#endif /* __TICTACTOE_H__ */

그리고 C++ 프로그램을 고려해서 이것도 추가한다.

#ifdef __cplusplus
extern "C" {
#endif /* __cplusplus */
.
.
.
#ifdef __cplusplus
}
#endif /* __cplusplus */

함수와 구조체들에 대해, 우리는 헤더파일 내에서 세가지 표준적인 매크로를 선언한다. 즉 TICTACTOE(obj), TICTACTOE_CLASS(klass), IS_TICTACTOE(obj)이다. 이들은 어떤 한 포인터를 object 혹은 class structure에 대한 포인터로 캐스트 하고, 어떤 object가 Tictactoe widget인지를 체크하는 역할을 한다.

이것이 최종적인 헤더파일이다.

/* tictactoe.h */

#ifndef __TICTACTOE_H__
#define __TICTACTOE_H__

#include <gdk/gdk.h>
#include <gtk/gtkvbox.h>

#ifdef __cplusplus
extern "C" {
#endif /* __cplusplus */

#define TICTACTOE(obj)          GTK_CHECK_CAST (obj, tictactoe_get_type (), Tictactoe)
#define TICTACTOE_CLASS(klass)  GTK_CHECK_CLASS_CAST (klass, tictactoe_get_type (), TictactoeClass)
#define IS_TICTACTOE(obj)       GTK_CHECK_TYPE (obj, tictactoe_get_type ())

typedef struct _Tictactoe       Tictactoe;
typedef struct _TictactoeClass  TictactoeClass;

struct _Tictactoe
{
        GtkVBox vbox;

        GtkWidget *buttons[3][3];
};

struct _TictactoeClass
{
        GtkVBoxClass parent_class;

        void (* tictactoe) (Tictactoe *ttt);
};

guint          tictactoe_get_type        (void);
GtkWidget*     tictactoe_new             (void);
void           tictactoe_clear           (Tictactoe *ttt);

#ifdef __cplusplus
}
#endif /* __cplusplus */

#endif /* __TICTACTOE_H__ */

_get_type() 함수

이제 우리의 widget을 계속 다듬어 보자. 모든 widget들에 대한 핵심(core) 함수는 WIDGETNAME_get_type()이다. 이 함수는 처음 호출되었을 때, GTK에게 widget class에 대해 알려준다. 그리고 widget class를 명확히 식별하는 ID를 취한다. 계속 호출하면 바로 그 ID를 리턴한다.

guint
tictactoe_get_type ()
{
  static guint ttt_type = 0;

  if (!ttt_type)
    {
      GtkTypeInfo ttt_info =
      {
        "Tictactoe",
        sizeof (Tictactoe),
        sizeof (TictactoeClass),
        (GtkClassInitFunc) tictactoe_class_init,
        (GtkObjectInitFunc) tictactoe_init,
        (GtkArgSetFunc) NULL,
        (GtkArgGetFunc) NULL
      };

      ttt_type = gtk_type_unique (gtk_vbox_get_type (), &ttt_info);
    }

  return ttt_type;
}

구조체 GtkTypeInfo는 다음과 같이 정의되었다.

struct _GtkTypeInfo
{
  gchar *type_name;
  guint object_size;
  guint class_size;
  GtkClassInitFunc class_init_func;
  GtkObjectInitFunc object_init_func;
  GtkArgSetFunc arg_set_func;
  GtkArgGetFunc arg_get_func;
};

이 구조체의 각 필드는 역시 보이는 그대로다. 우리는 여기서 arg_set_funcarg_get_func 필드 를 무시할 것이다. 이것은 인터프리터 언어에서 비롯되어 widget 옵션들을 편리하게 세팅할 수 있도록 하는 중요한 역할을 할 수 있지만, 아직은 대개 갖추어지지 않았다. 일단 GTK가 이 구조체의 제대로 된 복사본을 가지게 되면, 그것은 특별한 widget type의 object를 만드는 방법을 알게 된다.

_class_init() 함수

WIDGETNAME_class_init() 함수는 widget의 class structure에 있는 필드들을 초기화하고, 그 클래스에 대한 시그널들도 셋업해 준다. 우리의 Tictactoe widget에 대해서 이것은 이렇게 보여진다.


enum {
  TICTACTOE_SIGNAL,
  LAST_SIGNAL
};

static gint tictactoe_signals[LAST_SIGNAL] = { 0 };

static void
tictactoe_class_init (TictactoeClass *class)
{
  GtkObjectClass *object_class;

  object_class = (GtkObjectClass*) class;
  
  tictactoe_signals[TICTACTOE_SIGNAL] = gtk_signal_new ("tictactoe",
                                         GTK_RUN_FIRST,
                                         object_class->type,
                                         GTK_SIGNAL_OFFSET (TictactoeClass, tictactoe),
                                         gtk_signal_default_marshaller, GTK_TYPE_NONE, 0);


  gtk_object_class_add_signals (object_class, tictactoe_signals, LAST_SIGNAL);

  class->tictactoe = NULL;
}

우리 widget이 가진 유일한 시그널은 ``tictactoe'' 시그널로, 이것은 한 행, 열, 혹은 대각 성분이 완전히 채워지면 요구된다. 모든 합성 widget들이 시그널 을 필요로 하는 것은 아니며, 따라서 여러분이 이 부분을 처음 읽고 있는 상태 라면 다음 섹션(section)으로 넘어가도 좋다. 이 부분은 다소 복잡할 것이기 때문이다.

이 함수를 보자.

gint   gtk_signal_new (gchar               *name,
                                         GtkSignalRunType     run_type,
                                         gint                 object_type,
                                         gint                 function_offset,
                                         GtkSignalMarshaller  marshaller,
                                         GtkArgType           return_val,
                                         gint                 nparams,
                                         ...);

새로운 시그널을 만든다. 여기에 쓰인 인자들을 살펴보자.

타입을 설정하기 위해, GtkType이라는 enumeration이 쓰인다.

typedef enum
{
  GTK_TYPE_INVALID,
  GTK_TYPE_NONE,
  GTK_TYPE_CHAR,
  GTK_TYPE_BOOL,
  GTK_TYPE_INT,
  GTK_TYPE_UINT,
  GTK_TYPE_LONG,
  GTK_TYPE_ULONG,
  GTK_TYPE_FLOAT,
  GTK_TYPE_DOUBLE,
  GTK_TYPE_STRING,
  GTK_TYPE_ENUM,
  GTK_TYPE_FLAGS,
  GTK_TYPE_BOXED,
  GTK_TYPE_FOREIGN,
  GTK_TYPE_CALLBACK,
  GTK_TYPE_ARGS,

  GTK_TYPE_POINTER,

  /* it'd be great if the next two could be removed eventually */
  GTK_TYPE_SIGNAL,
  GTK_TYPE_C_CALLBACK,

  GTK_TYPE_OBJECT

} GtkFundamentalType;

gtk_signal_new()는 시그널에 대해 고유한 정수 식별자를 리턴한다. 우리는 이들을 tictactoe_signals 배열에 저장하고, enumeration로 이용해서 인덱스를 준다. (관습적으로 enumeration의 원소들은 대문자로 시그널의 이름을 나타내 지만, 여기서 TICTACTOE() 매크로와 충돌할 수 있기 때문에, 우리는 이들을 TICTACTOE_SIGNAL이라고 부른다.)

시그널을 만들었다면, 이제 GTK가 이 시그널들을 Tictactoe 클래스에 연결시키 도록 해야 한다. 이 작업은 gtk_object_class_add_signals()로 해준다. 그리고 아무런 디폴트 동작이 없다는 걸 나타내기 위해, ``tictactoe'' 시그널을 위한 디폴트 핸들러를 가리키고 있는 포인터를 NULL로 세팅한다.

_init() 함수

각 widget 클래스는 또한 object structure를 초기화해 줄 함수를 필요로 한다. 보통, 이 함수는 구조체의 필드들을 디폴트 값으로 세팅하는 나름대로 제한된 역할을 가지고 있다. 그러나 합성 widget들의 경우, 이 함수들이 적합한 또다른 widget들을 만들어 주기도 한다.

static void
tictactoe_init (Tictactoe *ttt)
{
        GtkWidget *table;
        gint i,j;

        table = gtk_table_new (3, 3, TRUE);
        gtk_container_add (GTK_CONTAINER(ttt), table);
        gtk_widget_show (table);

        for (i=0;i<3; i++)
                for (j=0;j<3; j++)
                        {
                                ttt->buttons[i][j] = gtk_toggle_button_new ();
                                gtk_table_attach_defaults (GTK_TABLE(table), ttt->buttons[i][j],
                                                                                 i, i+1, j, j+1);
                                gtk_signal_connect (GTK_OBJECT (ttt->buttons[i][j]), "toggled",
                                                                        GTK_SIGNAL_FUNC (tictactoe_toggle), ttt);
                                gtk_widget_set_usize (ttt->buttons[i][j], 20, 20);
                                gtk_widget_show (ttt->buttons[i][j]);
                        }
}

그리고 나머지들...

모든 widget들(GtkBin처럼 달리 설명될 수 없는 base widget들은 제외)이 가지고 있어야 할 함수가 하나 더 있다. 바로 그 해당하는 타입의 object를 만들기 위한 함수다. 이것은 관례상 WIDGETNAME_new()가 된다. Tictactoe widget에는 해당하지 않지만, 일부 widget들은 이 함수들이 인자를 가지고, 그리고 그 인자를 참고해서 어떤 셋업을 행하기도 한다. 다른 두 함수는 우리의 Tictactoe widget에 대한 특별한 것들이다.

tictactoe_clear()는 widget에 있는 모든 버튼을 up 상태로 리셋해 준다. 버튼 토글을 위한 우리 시그널 핸들러가 불필요하게 조준되는 것을 막아주기 위해 gtk_signal_handler_block_by_data()를 이용하는 것을 명심하자.

tictactoe_toggle()은 사용자가 어떤 버튼을 클릭했을 때 요청되는 시그널 핸들러다. 이것은 토글된 버튼을 포함해 어떤 매력적인 콤비(combination)가 이루어지는지 체크하고, 만약 그렇다면 "tictactoe" 시그널을 발생시킨다.

  
GtkWidget*
tictactoe_new ()
{
  return GTK_WIDGET ( gtk_type_new (tictactoe_get_type ()));
}

void           
tictactoe_clear (Tictactoe *ttt)
{
  int i,j;

  for (i=0;i<3;i++)
    for (j=0;j<3;j++)
      {
        gtk_signal_handler_block_by_data (GTK_OBJECT(ttt->buttons[i][j]), ttt);
        gtk_toggle_button_set_state (GTK_TOGGLE_BUTTON (ttt->buttons[i][j]),
                                     FALSE);
        gtk_signal_handler_unblock_by_data (GTK_OBJECT(ttt->buttons[i][j]), ttt);
      }
}

static void
tictactoe_toggle (GtkWidget *widget, Tictactoe *ttt)
{
  int i,k;

  static int rwins[8][3] = { { 0, 0, 0 }, { 1, 1, 1 }, { 2, 2, 2 },
                             { 0, 1, 2 }, { 0, 1, 2 }, { 0, 1, 2 },
                             { 0, 1, 2 }, { 0, 1, 2 } };
  static int cwins[8][3] = { { 0, 1, 2 }, { 0, 1, 2 }, { 0, 1, 2 },
                             { 0, 0, 0 }, { 1, 1, 1 }, { 2, 2, 2 },
                             { 0, 1, 2 }, { 2, 1, 0 } };

  int success, found;

  for (k=0; k<8; k++)
    {
      success = TRUE;
      found = FALSE;

      for (i=0;i<3;i++)
        {
          success = success && 
            GTK_TOGGLE_BUTTON(ttt->buttons[rwins[k][i]][cwins[k][i]])->active;
          found = found ||
            ttt->buttons[rwins[k][i]][cwins[k][i]] == widget;
        }
      
      if (success && found)
        {
          gtk_signal_emit (GTK_OBJECT (ttt), 
                           tictactoe_signals[TICTACTOE_SIGNAL]);
          break;
        }
    }
}

그리고 우리의 Tictactoe widget을 이용한 예제 프로그램은 최종적으로 이것이다.

#include <gtk/gtk.h>
#include "tictactoe.h"

/* 어떤 행, 열, 혹은 대각성분이 다 차게 되면 요청된다. */
void
win (GtkWidget *widget, gpointer data)
{
        g_print ("Yay!\n");
        tictactoe_clear (TICTACTOE (widget));
}

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

        gtk_init (&argc, &argv);

        window = gtk_window_new (GTK_WINDOW_TOPLEVEL);

        gtk_window_set_title (GTK_WINDOW (window), "Aspect Frame");

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

        gtk_container_border_width (GTK_CONTAINER (window), 10);

        /* Tictactoe widget을 하나 만든다. */
        ttt = tictactoe_new ();
        gtk_container_add (GTK_CONTAINER (window), ttt);
        gtk_widget_show (ttt);

        /* 그리고 이것의 "tictactoe" 시그널에 결합시켜 둔다. */
        gtk_signal_connect (GTK_OBJECT (ttt), "tictactoe",
                                                GTK_SIGNAL_FUNC (win), NULL);

        gtk_widget_show (window);

        gtk_main ();

        return 0;
}

20.4 무에서(from scratch) widget 만들기

소개

이번 섹션에서는 widget들이 그 자신들을 어떻게 스크린에 보이게 하는지, 그리고 이벤트와 상호작용 하는지를 더 공부할 것이다. 이에 대한 예제로 우리는 포인터가 달려있는 아날로그 다이얼(analog dial) widget을 만들어서, 사용자가 그걸 끌어서 값을 세팅하도록 할 것이다.

스크린에 widget을 보이기

스크린에 보여주기 위해서 몇가지 단계가 있다. Widget이 WIDGETNAME_new() 로써 만들어지고 나서, 몇개의 함수가 더 필요하다.

상당히 유사한 마지막의 두 함수에 주의해야 할 것이다. 사실 많은 타입의 widget들은 두 경우의 차이점을 그다지 구별하지 않는다. Widget 클래스의 디폴트 draw() 함수는 단지 다시 그려진 영역에 대해 종합적인 expose 이벤트를 발생시킬 뿐이다. 그러나 어떤 타입의 widget은 두 함수를 구별함으로써 작업을 최소화하기도 한다. 예를들어 여러 개의 X윈도를 가진 widget은 영향을 받은 (affected) 윈도만을 다시 그려줄 수 있는데, 이것은 단순히 draw()을 호출하는 것으로는 불가능한 일이다.

컨테이너 widget들은 서로의 차이점에 대해 자기 자신은 상관하지 않지만, 그들의 child widget들은 서로를 분명히 구별해야 하므로, 간단히 디폴트 draw() 함수를 가질 수 없다. 하지만, 두 함수 사이에서 그리는 코드를 중복하는 것은 낭비다. 이런 widget들은 관습상 WIDGETNAME_paint() 라고 불리는 함수를 가진다. 이 함수들은 widget을 실제로 그리는 역할을 하며, draw()expose() 함수에 의해 불리어지게 된다.

우리의 예제에서는 dial widget이 컨테이너가 아니며 오직 하나의 윈도만 가지 기 때문에, 우리는 간단하게 문제를 해결할 수 있다. 즉 디폴트 draw()를 이용 하고 또 expose() 함수만을 갖춘다.

Dial Widget의 기원

지상의 모든 동물들이 진흙탕에서 처음으로 기어나온 양서류의 변종들인 것처럼, Gtk widget들도 먼저 만들어진 widget의 변형에서 출발하는 경향이 있다. 그래서, 이 절(section)이 "Creating a Widget from Scratch"라는 제목으로 되어 있음에도, Dial widget은 실제로 Range widget의 코드에서 출발한다. 이는 역시 Range widget에서 파생된 Scale widget들과 똑같은 인터페이스를, 우리의 Dial widget이 가질 수 있다면 멋질 것이라는 생각에서다. 또한 만약 여러분이 어플 제작자의 관점에서 scale widget들의 작용에 대해 익숙하지 않다면, 먼저 그들에 대해 알아 보는 것도 좋은 생각일 것이다.

기초

우리 widget의 상당 부분은 Tictactoe widget과 꽤 유사하게 보일 것이다. 먼저 헤더 파일을 보자.

/* GTK - The GIMP Toolkit
 * Copyright (C) 1995-1997 Peter Mattis, Spencer Kimball and Josh MacDonald
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the Free
 * Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

#ifndef __GTK_DIAL_H__
#define __GTK_DIAL_H__

#include <gdk/gdk.h>
#include <gtk/gtkadjustment.h>
#include <gtk/gtkwidget.h>

#ifdef __cplusplus
extern "C" {
#endif /* __cplusplus */

#define GTK_DIAL(obj)          GTK_CHECK_CAST (obj, gtk_dial_get_type (), GtkDial)
#define GTK_DIAL_CLASS(klass)  GTK_CHECK_CLASS_CAST (klass, gtk_dial_get_type (), GtkDialClass)
#define GTK_IS_DIAL(obj)       GTK_CHECK_TYPE (obj, gtk_dial_get_type ())

typedef struct _GtkDial        GtkDial;
typedef struct _GtkDialClass   GtkDialClass;

struct _GtkDial
{
        GtkWidget widget;

        /* policy를 업데이트한다(GTK_UPDATE_[CONTINUOUS/DELAYED/DISCONTINUOUS]). */
        guint policy : 2;

        /* 현재 눌러진 버튼, 눌리지 않았다면 0. */
        guint8 button;

        /* 다이얼 구성요소들의 치수. */
        gint radius;
        gint pointer_width;

        /* 업데이트 타이머의 ID, 업데이트가 없으면 0. */
        guint32 timer;

        /* 현재의 각도. */
        gfloat angle;

        /* Adjustment가 저장한 이전 값들로, 우리가 어떤 변화를 알아낼 수 있다. */
        gfloat old_value;
        gfloat old_lower;
        gfloat old_upper;

        /* 다이얼의 데이터를 저장하는 adjustment object다. */
        GtkAdjustment *adjustment;
};

struct _GtkDialClass
{
        GtkWidgetClass parent_class;
};

GtkWidget*     gtk_dial_new                    (GtkAdjustment *adjustment);
guint          gtk_dial_get_type               (void);
GtkAdjustment* gtk_dial_get_adjustment         (GtkDial      *dial);
void           gtk_dial_set_update_policy      (GtkDial      *dial,
                                                                                                GtkUpdateType  policy);

void           gtk_dial_set_adjustment         (GtkDial      *dial,
                                                                                                GtkAdjustment *adjustment);
#ifdef __cplusplus
}
#endif /* __cplusplus */

#endif /* __GTK_DIAL_H__ */

이것이 이 widget의 전부가 아니고 데이터 구조체에 필드들이 더 있지만, 다른 것들 또한 여기 보인 것과 비슷하다.

이제 헤더파일을 포함(include)하고, 몇 개의 상수(constant)를 선언하고, widget에 대한 정보를 제공하고 그것을 초기화해 주는 함수들이 있다.

#include <math.h>
#include <stdio.h>
#include <gtk/gtkmain.h>
#include <gtk/gtksignal.h>

#include "gtkdial.h"

#define SCROLL_DELAY_LENGTH  300
#define DIAL_DEFAULT_SIZE 100

/* 앞 부분의 선언들 */

[ 공간을 아끼기 위해 생략됨. ]

/* 로컬 데이터 */

static GtkWidgetClass *parent_class = NULL;

guint
gtk_dial_get_type ()
{
        static guint dial_type = 0;

        if (!dial_type)
                {
                        GtkTypeInfo dial_info =
                        {
                                "GtkDial",
                                sizeof (GtkDial),
                                sizeof (GtkDialClass),
                                (GtkClassInitFunc) gtk_dial_class_init,
                                (GtkObjectInitFunc) gtk_dial_init,
                                (GtkArgFunc) NULL,
                        };

                        dial_type = gtk_type_unique (gtk_widget_get_type (), &dial_info);
                }

        return dial_type;
}

static void
gtk_dial_class_init (GtkDialClass *class)
{
        GtkObjectClass *object_class;
        GtkWidgetClass *widget_class;

        object_class = (GtkObjectClass*) class;
        widget_class = (GtkWidgetClass*) class;

        parent_class = gtk_type_class (gtk_widget_get_type ());

        object_class->destroy = gtk_dial_destroy;

        widget_class->realize = gtk_dial_realize;
        widget_class->expose_event = gtk_dial_expose;
        widget_class->size_request = gtk_dial_size_request;
        widget_class->size_allocate = gtk_dial_size_allocate;
        widget_class->button_press_event = gtk_dial_button_press;
        widget_class->button_release_event = gtk_dial_button_release;
        widget_class->motion_notify_event = gtk_dial_motion_notify;
}

static void
gtk_dial_init (GtkDial *dial)
{
        dial->button = 0;
        dial->policy = GTK_UPDATE_CONTINUOUS;
        dial->timer = 0;
        dial->radius = 0;
        dial->pointer_width = 0;
        dial->angle = 0.0;
        dial->old_value = 0.0;
        dial->old_lower = 0.0;
        dial->old_upper = 0.0;
        dial->adjustment = NULL;
}

GtkWidget*
gtk_dial_new (GtkAdjustment *adjustment)
{
        GtkDial *dial;

        dial = gtk_type_new (gtk_dial_get_type ());

        if (!adjustment)
                adjustment = (GtkAdjustment*) gtk_adjustment_new (0.0, 0.0, 0.0, 0.0, 0.0, 0.0);

        gtk_dial_set_adjustment (dial, adjustment);

        return GTK_WIDGET (dial);
}

static void
gtk_dial_destroy (GtkObject *object)
{
        GtkDial *dial;

        g_return_if_fail (object != NULL);
        g_return_if_fail (GTK_IS_DIAL (object));

        dial = GTK_DIAL (object);

        if (dial->adjustment)
                gtk_object_unref (GTK_OBJECT (dial->adjustment));

        if (GTK_OBJECT_CLASS (parent_class)->destroy)
                (* GTK_OBJECT_CLASS (parent_class)->destroy) (object);
}

이건 합성 widget이 아니므로 여기서의 init()함수는 Tictactoe widget에 대해서보다 더 적은 작업을 행한다. 그리고 인자를 가지게 되었으므로 new() 함수는 더 많은 작업을 한다. 또한, 우리가 Adjustment object를 향한 포인터를 저장할 때마다 그것의 reference count를 증가시킴을 기억하라. (이것을 더이상 이용하지 않을 때는 반대로 감소시킨다.) 그래서 GTK는 그것이 안전하게 파괴될 수 있도록 트랙을 유지할 수 있다.

Widget의 옵션을 다룰 수 있는 몇 개의 함수도 있다.

GtkAdjustment*
gtk_dial_get_adjustment (GtkDial *dial)
{
        g_return_val_if_fail (dial != NULL, NULL);
        g_return_val_if_fail (GTK_IS_DIAL (dial), NULL);

        return dial->adjustment;
}

void
gtk_dial_set_update_policy (GtkDial      *dial,
                                                         GtkUpdateType  policy)
{
        g_return_if_fail (dial != NULL);
        g_return_if_fail (GTK_IS_DIAL (dial));

        dial->policy = policy;
}

void
gtk_dial_set_adjustment (GtkDial      *dial,
                                                GtkAdjustment *adjustment)
{
        g_return_if_fail (dial != NULL);
        g_return_if_fail (GTK_IS_DIAL (dial));

        if (dial->adjustment)
                {
                        gtk_signal_disconnect_by_data (GTK_OBJECT (dial->adjustment), (gpointer) dial);
                        gtk_object_unref (GTK_OBJECT (dial->adjustment));
                }

        dial->adjustment = adjustment;
        gtk_object_ref (GTK_OBJECT (dial->adjustment));

        gtk_signal_connect (GTK_OBJECT (adjustment), "changed",
                                                (GtkSignalFunc) gtk_dial_adjustment_changed,
                                                (gpointer) dial);
        gtk_signal_connect (GTK_OBJECT (adjustment), "value_changed",
                                                (GtkSignalFunc) gtk_dial_adjustment_value_changed,
                                                (gpointer) dial);

        dial->old_value = adjustment->value;
        dial->old_lower = adjustment->lower;
        dial->old_upper = adjustment->upper;

        gtk_dial_update (dial);
}

gtk_dial_realize()

이제 새로운 타입의 함수들을 만나보자. 먼저, X윈도를 만드는 작업을 해주는 함수다. 함수 gdk_window_new()로 한 마스크가 넘겨진 것을 주목하라. 이것은 GdkWindowAttr 구조체의 어느 필드가 실제로 데이터를 가질 것인지 설정해 주는 것이다. 나머지 필드들은 디폴트 값으로 채워지게 된다. 또한 눈여겨 봐둘 만한 것은 widget의 이벤트 마스크가 설정되는 방법이다. 우리는 gtk_widget_get_events()로써 이 widget에 대해 사용자가 설정해 놓은 이벤트 마스크를 복구해 줄 수 있다(gtk_widget_set_events()로, 그리고 우리가 원하는 이벤트를 더해 준다).

윈도를 만들었다면, 우리는 그것의 스타일과 배경을 세팅하고, 그리고 그 widget을 향한 포인터를 GdkWindow의 user data 필드에 놓는다. 이 마지막 단계는 GTK가 적절한 widget으로 이 윈도에 대한 이벤트를 전파할 수 있도록 한다.

static void
gtk_dial_realize (GtkWidget *widget)
{
        GtkDial *dial;
        GdkWindowAttr attributes;
        gint attributes_mask;

        g_return_if_fail (widget != NULL);
        g_return_if_fail (GTK_IS_DIAL (widget));

        GTK_WIDGET_SET_FLAGS (widget, GTK_REALIZED);
        dial = GTK_DIAL (widget);

        attributes.x = widget->allocation.x;
        attributes.y = widget->allocation.y;
        attributes.width = widget->allocation.width;
        attributes.height = widget->allocation.height;
        attributes.wclass = GDK_INPUT_OUTPUT;
        attributes.window_type = GDK_WINDOW_CHILD;
        attributes.event_mask = gtk_widget_get_events (widget) |
                GDK_EXPOSURE_MASK | GDK_BUTTON_PRESS_MASK |
                GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK |
                GDK_POINTER_MOTION_HINT_MASK;
        attributes.visual = gtk_widget_get_visual (widget);
        attributes.colormap = gtk_widget_get_colormap (widget);

        attributes_mask = GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL | GDK_WA_COLORMAP;
        widget->window = gdk_window_new (widget->parent->window, &attributes, attributes_mask);

        widget->style = gtk_style_attach (widget->style, widget->window);

        gdk_window_set_user_data (widget->window, widget);

        gtk_style_set_background (widget->style, widget->window, GTK_STATE_ACTIVE);
}

크기 결정

어떤 widget을 포함한 윈도가 처음 보여지게 되기에 앞서, 그리고 윈도의 layout이 변했을 때도 언제나, GTK는 그것의 기대된 크기대로 각 child widget을 요구한다. 이 요청은 함수 gtk_dial_size_request()에 의해 다루어진다. 우리 widget은 컨테이너 widget이 아니고 또 크기에 대해 어떤 제한 조건도 없으므로, 우리는 단지 합당한 디폴트 값을 리턴해 준다.

static void
gtk_dial_size_request (GtkWidget      *widget,
                                         GtkRequisition *requisition)
{
        requisition->width = DIAL_DEFAULT_SIZE;
        requisition->height = DIAL_DEFAULT_SIZE;
}

모든 widget들이 이상적인 크기로 요청된 후, 윈도의 layout이 계산되고 각 child widget은 그것의 실제 크기로 통지받는다. 보통 이것은 최소한 요청된 크기만큼 된다. 하지만 사용자가 윈도를 resize하는 경우처럼, 간혹 요청된 크기보다 작아질 때도 있다. 크기의 통지(notification)는 함수 gtk_dial_size_allocate()로 다룬다. 앞으로의 이용을 위해 일부 구성 요소 조각들의 크기를 계산할 뿐 아니라, 이들 함수들은 새로운 위치와 크기로 widget의 X윈도를 옮겨 주는 역할을 한다는 것을 기억하자.

static void
gtk_dial_size_allocate (GtkWidget     *widget,
                                                GtkAllocation *allocation)
{
        GtkDial *dial;

        g_return_if_fail (widget != NULL);
        g_return_if_fail (GTK_IS_DIAL (widget));
        g_return_if_fail (allocation != NULL);

        widget->allocation = *allocation;
        if (GTK_WIDGET_REALIZED (widget))
                {
                        dial = GTK_DIAL (widget);

                        gdk_window_move_resize (widget->window,
                                                                        allocation->x, allocation->y,
                                                                        allocation->width, allocation->height);

                        dial->radius = MAX(allocation->width,allocation->height) * 0.45;
                        dial->pointer_width = dial->radius / 5;
                }
}

gtk_dial_expose()

앞에서 언급했듯이, 이 widget의 모든 그리기 작업은 expose 이벤트에 대한 핸들러에서 행해진다. 여기서 주목할 것은 한 가지 뿐이다. Widget의 스타일에 저장된 색깔들에 따라 3차원으로 그림자 진 포인터를 그리기 위해, gtk_draw_polygon을 이용한다는 것이다.

static gint
gtk_dial_expose (GtkWidget      *widget,
                                 GdkEventExpose *event)
{
        GtkDial *dial;
        GdkPoint points[3];
        gdouble s,c;
        gdouble theta;
        gint xc, yc;
        gint tick_length;
        gint i;

        g_return_val_if_fail (widget != NULL, FALSE);
        g_return_val_if_fail (GTK_IS_DIAL (widget), FALSE);
        g_return_val_if_fail (event != NULL, FALSE);

        if (event->count > 0)
                return FALSE;

        dial = GTK_DIAL (widget);

        gdk_window_clear_area (widget->window,
                                                 0, 0,
                                                 widget->allocation.width,
                                                 widget->allocation.height);

        xc = widget->allocation.width/2;
        yc = widget->allocation.height/2;

        /* 시계 눈금을 그린다. */

        for (i=0; i<25; i++)
                {
                        theta = (i*M_PI/18. - M_PI/6.);
                        s = sin(theta);
                        c = cos(theta);

                        tick_length = (i%6 == 0) ? dial->pointer_width : dial->pointer_width/2;

                        gdk_draw_line (widget->window,
                                                 widget->style->fg_gc[widget->state],
                                                 xc + c*(dial->radius - tick_length),
                                                 yc - s*(dial->radius - tick_length),
                                                 xc + c*dial->radius,
                                                 yc - s*dial->radius);
                }

        /* 포인터를 그린다. */

        s = sin(dial->angle);
        c = cos(dial->angle);

        points[0].x = xc + s*dial->pointer_width/2;
        points[0].y = yc + c*dial->pointer_width/2;
        points[1].x = xc + c*dial->radius;
        points[1].y = yc - s*dial->radius;
        points[2].x = xc - s*dial->pointer_width/2;
        points[2].y = yc - c*dial->pointer_width/2;

        gtk_draw_polygon (widget->style,
                      widget->window,
                      GTK_STATE_NORMAL,
                      GTK_SHADOW_OUT,
                      points, 3,
                      TRUE);
        return FALSE;
}

이벤트 다루기

Widget의 코드에서 나머지 코드는 다양한 형태의 이벤트를 다룬다. 또한 이것은 수많은 GTK 어플들 사이에서 그다지 큰 차이가 나지 않는다. 이벤트는 크게 두가지 형태로 나눌 수 있다. 먼저 사용자가 widget 위에서 마우스를 클릭하거나 포인터를 이동시키려고 드래그를 할 수 있다. 그리고 외부적인 상황에 의해, Adjustment object의 값이 변해 버릴 경우가 있다.

사용자가 widget 위에서 클릭을 하면, 우리는 클릭이 적절히 포인터 근처에서 이루어졌는지 체크하고, 만약 그렇다면 widget 구조체의 버튼 필드에 그 눌려진 버튼을 저장하고, 그리고 gtk_grab_add() 호출로써 모든 마우스 이벤트를 잡아 챈다. 뒤따르는 마우스의 동작은 제어값들이 다시 계산되어지게 한다(gtk_dial_update_mouse 함수로써). 세팅된 policy에 따라, "value_changed" 이벤트는 다르게 발생한다. 즉 GTK_UPDATE_CONTINUOUS일 경우엔 연속적으로 계속 발생하고, GTK_UPDATE_DELAYED/일 경우엔 함수 gtk_timeout_add()로 더해지는 타이머만큼 지연되며 발생하며, GTK_UPDATE_DISCONTINUOUS일 경우엔 버튼이 release되는 순간에만 발생한다.

static gint
gtk_dial_button_press (GtkWidget      *widget,
           GdkEventButton *event)
{
  GtkDial *dial;
  gint dx, dy;
  double s, c;
  double d_parallel;
  double d_perpendicular;

  g_return_val_if_fail (widget != NULL, FALSE);
  g_return_val_if_fail (GTK_IS_DIAL (widget), FALSE);
  g_return_val_if_fail (event != NULL, FALSE);

  dial = GTK_DIAL (widget);

  /* 버튼의 눌림이 포인터 영역 내부에서 이루어졌나 체크한다.
   * 이것은 포인터의 위치와 마우스가 눌러진 위치의 수직 및 수평 거리를
   * 계산함으로써 이루어진다. */

  dx = event->x - widget->allocation.width / 2;
  dy = widget->allocation.height / 2 - event->y;

  s = sin(dial->angle);
  c = cos(dial->angle);

  d_parallel = s*dy + c*dx;
  d_perpendicular = fabs(s*dx - c*dy);

  if (!dial->button &&
      (d_perpendicular < dial->pointer_width/2) &&
      (d_parallel > - dial->pointer_width))
    {
      gtk_grab_add (widget);

      dial->button = event->button;

      gtk_dial_update_mouse (dial, event->x, event->y);
    }

  return FALSE;
}

static gint
gtk_dial_button_release (GtkWidget      *widget,
            GdkEventButton *event)
{
  GtkDial *dial;

  g_return_val_if_fail (widget != NULL, FALSE);
  g_return_val_if_fail (GTK_IS_DIAL (widget), FALSE);
  g_return_val_if_fail (event != NULL, FALSE);

  dial = GTK_DIAL (widget);

  if (dial->button == event->button)
    {
      gtk_grab_remove (widget);

      dial->button = 0;

      if (dial->policy == GTK_UPDATE_DELAYED)
        gtk_timeout_remove (dial->timer);

      if ((dial->policy != GTK_UPDATE_CONTINUOUS) &&
          (dial->old_value != dial->adjustment->value))
        gtk_signal_emit_by_name (GTK_OBJECT (dial->adjustment), "value_changed");
    }

  return FALSE;
}

static gint
gtk_dial_motion_notify (GtkWidget      *widget,
           GdkEventMotion *event)
{
  GtkDial *dial;
  GdkModifierType mods;
  gint x, y, mask;

  g_return_val_if_fail (widget != NULL, FALSE);
  g_return_val_if_fail (GTK_IS_DIAL (widget), FALSE);
  g_return_val_if_fail (event != NULL, FALSE);

  dial = GTK_DIAL (widget);

  if (dial->button != 0)
    {
      x = event->x;
      y = event->y;

      if (event->is_hint || (event->window != widget->window))
        gdk_window_get_pointer (widget->window, &x, &y, &mods);

      switch (dial->button)
        {
        case 1:
          mask = GDK_BUTTON1_MASK;
          break;
        case 2:
          mask = GDK_BUTTON2_MASK;
          break;
        case 3:
          mask = GDK_BUTTON3_MASK;
          break;
        default:
          mask = 0;
          break;
        }

      if (mods & mask)
        gtk_dial_update_mouse (dial, x,y);
    }

  return FALSE;
}

static gint
gtk_dial_timer (GtkDial *dial)
{
  g_return_val_if_fail (dial != NULL, FALSE);
  g_return_val_if_fail (GTK_IS_DIAL (dial), FALSE);

  if (dial->policy == GTK_UPDATE_DELAYED)
    gtk_signal_emit_by_name (GTK_OBJECT (dial->adjustment), "value_changed");

  return FALSE;
}

static void
gtk_dial_update_mouse (GtkDial *dial, gint x, gint y)
{
  gint xc, yc;
  gfloat old_value;

  g_return_if_fail (dial != NULL);
  g_return_if_fail (GTK_IS_DIAL (dial));

  xc = GTK_WIDGET(dial)->allocation.width / 2;
  yc = GTK_WIDGET(dial)->allocation.height / 2;

  old_value = dial->adjustment->value;
  dial->angle = atan2(yc-y, x-xc);

  if (dial->angle < -M_PI/2.)
    dial->angle += 2*M_PI;

  if (dial->angle < -M_PI/6)
    dial->angle = -M_PI/6;

  if (dial->angle > 7.*M_PI/6.)
    dial->angle = 7.*M_PI/6.;

  dial->adjustment->value = dial->adjustment->lower + (7.*M_PI/6 - dial->angle) *
    (dial->adjustment->upper - dial->adjustment->lower) / (4.*M_PI/3.);

  if (dial->adjustment->value != old_value)
    {
      if (dial->policy == GTK_UPDATE_CONTINUOUS)
        {
          gtk_signal_emit_by_name (GTK_OBJECT (dial->adjustment), "value_changed");
        }
      else
        {
          gtk_widget_draw (GTK_WIDGET(dial), NULL);

          if (dial->policy == GTK_UPDATE_DELAYED)
            {
              if (dial->timer)
                gtk_timeout_remove (dial->timer);

              dial->timer = gtk_timeout_add (SCROLL_DELAY_LENGTH,
                                             (GtkFunction) gtk_dial_timer,
                                             (gpointer) dial);
            }
        }
    }
}

그리고 외부 요인에 의한 Adjustment의 변화들은 ``changed''와 ``value_changed'' 시그널을 통해 우리 widget에 전달되는 것이다. 이런 함수들을 위한 핸들러들은 gtk_dial_update()를 호출해서, 인자들을 확인하고, 새로운 포인터 각도를 계산 하고, 그리고 widget을 다시 그려준다(gtk_widget_draw()를 호출해서).

static void
gtk_dial_update (GtkDial *dial)
{
  gfloat new_value;

  g_return_if_fail (dial != NULL);
  g_return_if_fail (GTK_IS_DIAL (dial));

  new_value = dial->adjustment->value;

  if (new_value < dial->adjustment->lower)
    new_value = dial->adjustment->lower;

  if (new_value > dial->adjustment->upper)
    new_value = dial->adjustment->upper;

  if (new_value != dial->adjustment->value)
    {
      dial->adjustment->value = new_value;
      gtk_signal_emit_by_name (GTK_OBJECT (dial->adjustment), "value_changed");
    }

  dial->angle = 7.*M_PI/6. - (new_value - dial->adjustment->lower) * 4.*M_PI/3. /
    (dial->adjustment->upper - dial->adjustment->lower);

  gtk_widget_draw (GTK_WIDGET(dial), NULL);
}

static void
gtk_dial_adjustment_changed (GtkAdjustment *adjustment,
                              gpointer       data)
{
  GtkDial *dial;

  g_return_if_fail (adjustment != NULL);
  g_return_if_fail (data != NULL);

  dial = GTK_DIAL (data);

  if ((dial->old_value != adjustment->value) ||
      (dial->old_lower != adjustment->lower) ||
      (dial->old_upper != adjustment->upper))
    {
      gtk_dial_update (dial);

      dial->old_value = adjustment->value;
      dial->old_lower = adjustment->lower;
      dial->old_upper = adjustment->upper;
    }
}

static void
gtk_dial_adjustment_value_changed (GtkAdjustment *adjustment,
                gpointer       data)
{
  GtkDial *dial;

  g_return_if_fail (adjustment != NULL);
  g_return_if_fail (data != NULL);

  dial = GTK_DIAL (data);

  if (dial->old_value != adjustment->value)
    {
      gtk_dial_update (dial);

      dial->old_value = adjustment->value;
    }
}

가능한 기능향상들

우리가 봤듯이 Dial widget은 약 670줄의 코드를 가지고 있다. 특히 이 코드 길이의 대부분이 헤더와 보일러판(boiler plate)이기 때문에, 우리는 이 긴 코드 에서 꽤 많은 것을 배울 수 있었다. 그러나 이 widget에 대해 가능한 몇 가지 기능개선이 있다.

20.5 더 배워보기

Widget을 만들기 위한 수많은 항목들 중 극히 일부만이 위에서 소개되었다. 자신의 widget을 만들려는 이에게, 가장 좋은 소스는 GTK 그 자체일 것이다. 여러분이 만들려는 widget에 대해 스스로 몇가지 질문을 해보자. 이것은 Container widget인가? 이것은 관련된 윈도를 가지고 있는가? 이것은 이미 존재하는 widget의 변형인가? 그리고는 유사한 widget을 찾고, 변화를 주기 시작하는 것이다. 행운을 빈다!


다음 이전 차례