다음 이전 차례

21. 낙서장, 간단한 그리기 예제

21.1 개요

여기서 우리는 간단한 그리기 프로그램을 만들 것이다. 그 과정에서 우리는 마우스 이벤트를 어떻게 다루는지 알아볼 것이고, 윈도 안에 어떻게 그림을 그리는지, 그리고 픽스맵을 배경으로 해서 더 나은 그림을 그리는 방법도 배울 것이다. 간단한 그리기 프로그램을 만든 후에, 우리는 그것에 drawing tablet 같은 XInput device를 지원하는 등의 확장을 시도할 것이다. GTK는 그런 device 들로부터 pressure와 tilt같은, 확장된 정보를 얻어낼 수 있게 하는 support routine을 제공한다.

21.2 이벤트 다루기

우리가 이미 살펴본 GTK의 시그널은 메뉴 아이템 선택처럼 고수준의 동작을 위한 것들이다. 하지만, 마우스의 움직임이라든지 키보드의 눌러짐같은, 저수준의 제어를 공부하는 것도 때때로 유용할 것이다. GTK 시그널 중에는 이런 저수준의 이벤트를 위한 것들도 있다. 이런 이벤트를 위한 핸들러는 그런 이벤트에 대한 정보를 가지고 있는 구조체를 가리키고 있는 또다른 인자들을 가지게 된다. 예를들어, motion event 핸들러는 아래와 같은 GdkEventMotion 구조체를 향한 포인터를 인자로 가지고 있다.

struct _GdkEventMotion
{
        GdkEventType type;
        GdkWindow *window;
        guint32 time;
        gdouble x;
        gdouble y;
        ...
        guint state;
        ...
};

인자 type은 이벤트의 타입이 되고, 이 경우에는 GDK_MOTION_NOTIFY이다. 인자 window는 그 이벤트가 발생한 윈도가 될 것이다. 인자 xy는 그 이벤트의 좌표이며, state는 이벤트가 발생했을 때의 modifier state를 설정한다(즉, 어떤 modifier key와 modifier button이 눌러졌는지 설정). 이것은 아래 보이는 상수값들을 비트 OR시킨 것이다.

GDK_SHIFT_MASK
GDK_LOCK_MASK
GDK_CONTROL_MASK
GDK_MOD1_MASK
GDK_MOD2_MASK
GDK_MOD3_MASK
GDK_MOD4_MASK
GDK_MOD5_MASK
GDK_BUTTON1_MASK
GDK_BUTTON2_MASK
GDK_BUTTON3_MASK
GDK_BUTTON4_MASK
GDK_BUTTON5_MASK

다른 시그널들에 대해서처럼, 이벤트가 발생했을 때 그것이 어떤 건지 결정 하기 위해 우리는 gtk_signal_connect()를 이용한다. 그러나 또한 우리가 어떤 이벤트가 탐지되어지길 원하는지 GTK가 알 수 있도록 해야한다. 이를 위해서는 이 함수를 이용한다.

void       gtk_widget_set_events          (GtkWidget           *widget,
                                           gint                 events);

두 번째 인자는 우리가 관심을 가진 이벤트를 설정한다. 이것은 여러 가지의 이벤트를 위해서 몇가지 상수를 비트 OR시킨 것이다. 앞으로의 참조를 위해서 이 상수들를 소개한다.

GDK_EXPOSURE_MASK
GDK_POINTER_MOTION_MASK
GDK_POINTER_MOTION_HINT_MASK
GDK_BUTTON_MOTION_MASK
GDK_BUTTON1_MOTION_MASK
GDK_BUTTON2_MOTION_MASK
GDK_BUTTON3_MOTION_MASK
GDK_BUTTON_PRESS_MASK
GDK_BUTTON_RELEASE_MASK
GDK_KEY_PRESS_MASK
GDK_KEY_RELEASE_MASK
GDK_ENTER_NOTIFY_MASK
GDK_LEAVE_NOTIFY_MASK
GDK_FOCUS_CHANGE_MASK
GDK_STRUCTURE_MASK
GDK_PROPERTY_CHANGE_MASK
GDK_PROXIMITY_IN_MASK
GDK_PROXIMITY_OUT_MASK

gtk_widget_set_events()를 호출할 때 관찰되어야 할 몇 개의 미묘한 사항이 있다. 먼저, 그것은 GTK widget을 위한 X윈도가 만들어지기 이전에 호출되어야 한다. 즉, 이것은 widget을 새로 만든 직후에 호출되어야 한다는 말이다. 둘째 로, 그 widget은 관련된 X윈도를 꼭 가져야 한다. 효율을 위해서 많은 widget type들은 그들만의 윈도를 가지지 않고 그들의 parent 윈도 안에 그려넣는다. 이런 widget은 다음과 같은 것들이다.

GtkAlignment
GtkArrow
GtkBin
GtkBox
GtkImage
GtkItem
GtkLabel
GtkPaned
GtkPixmap
GtkScrolledWindow
GtkSeparator
GtkTable
GtkViewport
GtkAspectFrame
GtkFrame
GtkVPaned
GtkHPaned
GtkVBox
GtkHBox
GtkVSeparator
GtkHSeparator

이런 이벤트들을 탐지하기 위해 우리는 EventBox widget을 필요로 한다. 자세 한 것은 The EventBox Widget을 참조하라.

우리의 그리기 프로그램에서는 마우스 버튼이 눌러지는 것과 움직이는 것을 탐지하고 싶어하므로, GDK_POINTER_MOTION_MASKGDK_BUTTON_PRESS_MASK를 설정한다. 우리는 또한 윈도가 다시 그려져야 할 때를 알아야 하므로 GDK_EXPOSURE_MASK도 설정한다. 비록 우리는 윈도의 크기가 변했을 때를 Configure event로써 탐지해야 하지만, 우리는 이를 위해 GDK_STRUCTURE_MASK flag를 설정할 필요가 없다. 이것은 모든 윈도에 대해 자동적으로 설정되기 때문이다.

그런데 GDK_POINTER_MOTION_MASK를 설정 하는 데 있어 문제점이 있다는 것이 밝혀질 것이다. 이것은 사용자가 마우스를 움직일 때마다 서버가 event queue에 motion event를 계속 더할 것이다. 어떤 motion event를 다루기 위해 0.1초가 걸린다고 생각해 보자. 그러나 X 서버는 매 0.05초마다 새로운 motion을 queue에 저장한다. 우리는 곧 사용자가 그리는 데 대한 방법을 가지게 될 것이다. 만약 사용자가 5초동안 그림을 그리면, 그들이 마우스 버튼을 놓고 난 후 또다른 5초가, 그것을 잡아내기 위해 필요하게 될 것이다. 우리가 원하는 것은 진행되는 각각의 이벤트에 대해 단 하나의 motion event를 탐지하는 것이다. 이렇게 하기 위해서 우리는 GDK_POINTER_MOTION_HINT_MASK를 설정한다.

우리가 GDK_POINTER_MOTION_HINT_MASK를 설정하면, 서버는 마우스 포인터가 우리의 윈도에 들어오고 나서 또는 버튼의 press나 release 이벤트가 발생하고 나서 처음으로 움직일 때 motion event를 보내준다. 뒤따르는 motion event는 우리가 아래의 함수로써 분명하게 마우스 포인터의 위치를 물어볼 때까지 억제될 것이다.

GdkWindow*    gdk_window_get_pointer     (GdkWindow       *window,
                                          gint            *x,
                                          gint            *y,
                                          GdkModifierType *mask);

(더 간단한 쓰임새를 가진 gtk_widget_get_pointer()라는 함수도 있지만, 그다지 유용하지는 않음이 드러날 것이다. 왜냐하면 그것은 마우스 버튼의 상태에 관계 없이 포인터의 위치만을 말해주기 때문이다.)

우리의 윈도에 이벤트를 세팅하는 코드는 그러므로 이런 식으로 될 것이다.

gtk_signal_connect (GTK_OBJECT (drawing_area), "expose_event",
          (GtkSignalFunc) expose_event, NULL);
gtk_signal_connect (GTK_OBJECT(drawing_area),"configure_event",
          (GtkSignalFunc) configure_event, NULL);
gtk_signal_connect (GTK_OBJECT (drawing_area), "motion_notify_event",
          (GtkSignalFunc) motion_notify_event, NULL);
gtk_signal_connect (GTK_OBJECT (drawing_area), "button_press_event",
          (GtkSignalFunc) button_press_event, NULL);
gtk_widget_set_events (drawing_area, GDK_EXPOSURE_MASK
                   | GDK_LEAVE_NOTIFY_MASK
                   | GDK_BUTTON_PRESS_MASK
                   | GDK_POINTER_MOTION_MASK
                   | GDK_POINTER_MOTION_HINT_MASK);

"expose_event"와 "configure_event"는 다음을 위해서 남겨둔다. "motion_ notify_event"와 "button_press_event"의 핸들러는 꽤 간단하다.

static gint
button_press_event (GtkWidget *widget, GdkEventButton *event)
{
  if (event->button == 1 && pixmap != NULL)
      draw_brush (widget, event->x, event->y);
  return TRUE;
}
static gint
motion_notify_event (GtkWidget *widget, GdkEventMotion *event)
{
  int x, y;
  GdkModifierType state;
  if (event->is_hint)
    gdk_window_get_pointer (event->window, &x, &y, &state);
  else
    {
      x = event->x;
      y = event->y;
      state = event->state;
    }
  if (state & GDK_BUTTON1_MASK && pixmap != NULL)
    draw_brush (widget, x, y);
  return TRUE;
}

21.3 DrawingArea widget, 그리고 그리기

이제 스크린에 그림을 그리는 과정에 대해 알아보자. 이때 필요한 것은 DrawingArea widget이다. 그림을 그리는 영역의 widget이란 본질적으로 다름 아닌 X윈도다. 이것은 우리가 원하는 것을 무엇이든 그려넣을 수 있는 빈 캔버스다. Drawing area는 이 함수를 이용해서 만들어진다.

GtkWidget* gtk_drawing_area_new        (void);

이 widget의 디폴트 크기는 이 함수로써 설정한다.

void       gtk_drawing_area_size       (GtkDrawingArea      *darea,
                                        gint                 width,
                                        gint                 height);

모든 widget에 대해 원한다면 이 디폴트 크기는 gtk_widget_set_usize()로써 오버로드될 수 있다. 그리고 사용자가 직접 그 drawing area를 포함한 윈도의 크기를 변경하면 역시 오버로드된다.

우리가 DrawingArea widget을 만들 때, 그릴 대상이 무엇인가에 전적으로 주의를 기울여야 한다. 만약 우리의 윈도가 감추어졌다가 다시 드러났다면, 우리는 exposure 마스크를 설정하고 그리고 원래 감추어졌었던 내용이 무엇이었는지 꼭 다시 그려줘야 한다. 덧붙여서, 윈도의 일부분이 지워졌다가 단계적으로 다시 그려지는 것은 시각적으로 산만해질 수 있다. 이런 문제에 대한 해법은 offscreen backing pixmap을 이용하는 것이다. 스크린 위로 바로 그리는 대신, 보이지 않는 서버 메모리에 저장된 어떤 이미지 위에 그려놓고, 이미지가 변했거나 또는 이미지의 새로운 영역이 보여지게 되면 해당하는 영역을 스크린에 복사해 주는 것이다.

어떤 offscreen pixmap을 만들기 위해, 우리는 이 함수를 이용한다.

GdkPixmap* gdk_pixmap_new (GdkWindow  *window,
                         gint        width,
                         gint        height,
                         gint        depth);

인자 window는 이 픽스맵이 특성을 이어받을 GDK 윈도를 설정한다. widthheight는 픽스맵의 크기를 정한다. depth는 color depth로서, 이 새로운 윈도에서의 한 픽셀당 비트의 수다. 만약 depth-1로 설정되면, 그것은 윈도의 depth와 똑같아진다.

우리는 픽스맵을 "configure_event" handler 안에 만든다. 이 이벤트는 윈도가 처음 생성될 때를 포함해서, 윈도의 크기가 변할 때마다 발생한다.

/* 그릴 영역에 대한 backing pixmap */
static GdkPixmap *pixmap = NULL;
/* 적당한 크기의 backing pixmap을 하나 만든다. */
static gint
configure_event (GtkWidget *widget, GdkEventConfigure *event)
{
    if (pixmap)
        {
            gdk_pixmap_destroy(pixmap);
        }
    pixmap = gdk_pixmap_new(widget->window,
                            widget->allocation.width,
                            widget->allocation.height,
                            -1);
    gdk_draw_rectangle (pixmap,
                    widget->style->white_gc,
                    TRUE,
                    0, 0,
                    widget->allocation.width,
                    widget->allocation.height);
    return TRUE;
}

gdk_draw_rectangle()을 부르며 픽스맵을 white로 초기화한다. 여기에 대해 잠깐 더 얘기할 것이다. 그러면 우리의 exposure event handler는 단순히 그 픽스맵의 관계된 portion을 스크린 위로 복사한다(우리는 exposure event의 event->area 필드로써 redraw할 영역을 결정한다).

/* Backing pixmap으로부터 스크린을 복구한다. */
static gint
expose_event (GtkWidget *widget, GdkEventExpose *event)
{
    gdk_draw_pixmap(widget->window,
                    widget->style->fg_gc[GTK_WIDGET_STATE (widget)],
                    pixmap,
                    event->area.x, event->area.y,
                    event->area.x, event->area.y,
                    event->area.width, event->area.height);
    return FALSE;
}

우리는 이제 픽스맵에 대해 어떻게 스크린을 보존하는지 보았다. 그런데 우리의 픽스맵에 실제로 재미있는 것들을 어떻게 그려넣을까? GTK의 GDK 라이브러리에는 drawables들을 그리는 데 필요한 많은 수의 함수들이 있다. Drawable이란 간단히 어딘가에 그려질 수 있는 그 무엇이다. 여기서 어디란 하나의 window, pixmap, 또는 bitmap(흑백의 이미지)이 될 수 있다. 우리는 이미 이런 함수 두가지를 보았다. gdk_draw_rectangle()gdk_draw_pixmap()이 그것이다. 이 함수들의 완전한 리스트는 이렇다.

gdk_draw_line ()
gdk_draw_rectangle ()
gdk_draw_arc ()
gdk_draw_polygon ()
gdk_draw_string ()
gdk_draw_text ()
gdk_draw_pixmap ()
gdk_draw_bitmap ()
gdk_draw_image ()
gdk_draw_points ()
gdk_draw_segments ()

이 함수들에 대해 더 자세히 알려면 참고 문서를 보거나 <gdk/gdk.h> 파일을 보라. 이 함수들은 처음 두 개의 인자를 공통적으로 가진다. 첫 번째는 그려 넣을 drawable, 그리고 두번째는 graphics context(GC)이다.

C는 foreground/background color나 line width같은, 그려질 것에 대한 정보들을 캡슐화한 것이다. GDK는 GC를 새로 만들고 변형하는데 필요한 모든 함수들을 가지고 있지만, 뭐든지 간단히 유지하기 위해서(Keep things simple) 우리는 이미 정의되어 있는 GC만을 쓸 것이다. 각 widget은 연관된 스타일이 있다(이들은 rc 파일을 편집함으로써 변형가능하다. GTK의 rc 파일에 대한 부분을 참조). 이것은 다른 것들 사이에서 GC의 개수를 저장한다. 이런 GC에 접근하는 몇가지 예를 보이겠다.

widget->style->white_gc
widget->style->black_gc
widget->style->fg_gc[GTK_STATE_NORMAL]
widget->style->bg_gc[GTK_WIDGET_STATE(widget)]

필드 fg_gc, bg_gc, dark_gc, light_gc 등은 아래 값들을 가질 수 있는 GtkStateType이라는 인자로 나타내어진다.

GTK_STATE_NORMAL,
GTK_STATE_ACTIVE,
GTK_STATE_PRELIGHT,
GTK_STATE_SELECTED,
GTK_STATE_INSENSITIVE

예를들어, GTK_STATE_SELECTED에 대해 디폴트 foreground 색깔은 white이고 background 색깔은 dark blue이다. 실제로 스크린 위로 그리는 함수인 draw_brush()는 그러므로 이렇게 된다.

/* 스크린에 사각형을 하나 그린다. */
static void
draw_brush (GtkWidget *widget, gdouble x, gdouble y)
{
    GdkRectangle update_rect;
    update_rect.x = x - 5;
    update_rect.y = y - 5;
    update_rect.width = 10;
    update_rect.height = 10;
    gdk_draw_rectangle (pixmap,
                        widget->style->black_gc,
                        TRUE,
                        update_rect.x, update_rect.y,
                        update_rect.width, update_rect.height);
    gtk_widget_draw (widget, &update_rect);
}

우리가 픽스맵 위로 브러쉬를 표시하는 사각형을 그리고 나서, 이 함수를 호출한다.

void       gtk_widget_draw              (GtkWidget           *widget,
                                         GdkRectangle        *area);

이것은 area라는 인자로 주어진 영역이 업데이트되어야 함을 X에게 알려준다. 결국 X는 expose 이벤트를 발생하고, 이것은 우리의 expose 이벤트 핸들러가 관계된 영역을 스크린 위로 복사하게끔 할 것이다.

이제 우리는 main 윈도를 만드는 것 같이 일부 흔한 항목들을 제외하고 이 그리기 프로그램을 모두 살펴보았다. 완전한 소스코드는 이 문서를 구한 곳이나 다음 위치에서 구할 수 있다.

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

21.4 Input support를 더하기

이제 drawing tablet 같이, 그다지 비싸지 않으면서 마우스보다 훨씬 예술적인 표현을 편하게 그릴 수 있게 해주는 입력장치(input device)를 구입하는 것이 가능한 시대다. 이런 장치를 이용하는 가장 간단한 방법이야 간단히 마우스를 대치해 버리는 것이겠지만, 이럴 경우 그런 입력장치의 다음과 같은 많은 장점을 잃게 될 수도 있다.

XInput extension에 대한 정보를 얻으려면 XInput-HOWTO 를 보라. 예를들어 GdkEventMotion 구조체의 전체 정의같은 걸 봐도, extended device를 지원하기 위한 필드를 가지고 있음을 알 것이다.

struct _GdkEventMotion
{
        GdkEventType type;
        GdkWindow *window;
        guint32 time;
        gdouble x;
        gdouble y;
        gdouble pressure;
        gdouble xtilt;
        gdouble ytilt;
        guint state;
        gint16 is_hint;
        GdkInputSource source;
        guint32 deviceid;
};

pressure 인자는 0과 1 사이의 부동소수점수로 된 pressure를 준다. xtiltytile는 각 방향으로의 tilt 각도에 해당하는 -1부터 1 사이의 값을 가질 수 있다. sourcedeviceid 인자는 두가지 다른 방식으로 발생한 이벤트에 대해 장치(device)를 설정한다. source는 장치의 타입에 대한 간단한 정보를 준다. 이것은 다음의 enumeration 값들을 가질 수 있다.

GDK_SOURCE_MOUSE
GDK_SOURCE_PEN
GDK_SOURCE_ERASER
GDK_SOURCE_CURSOR

deviceid는 각 장치에 대해 고유의 숫자 ID를 설정한다. 이것은 gdk_input_list_devices() 함수로(뒤에 나옴) 그 장치에 대한 더 많은 정보를 조사하고자 할 때 쓰일 수 있다. Core pointer device(보통 마우스)에 대해 특별히 GDK_CORE_POINTER라는 상수값이 쓰인다.

Extended device 정보를 사용가능하게 하기

GTK에게 우리가 관심을 가진 extended device의 정보가 무엇인지 알려 주려면, 우리 프로그램에 단 한 줄만 더해주면 된다.

gtk_widget_set_extension_events (drawing_area, GDK_EXTENSION_EVENTS_CURSOR);

GDK_EXTENSION_EVENTS_CURSOR라는 값을 줌으로써 우리는 확장된 이벤트들에 관심이 있음을 알린다. 단, 이때 우리는 새로 커서를 그려서는 안된다. 커서를 그리는 문제에 대해서는 뒤에 나올 복잡한 이용 부분을 참조하라. 우리가 우리만의 커서를 그리려 한다면 GDK_EXTENSION_EVENTS_ALL을 쓰면 되고, 디폴트 로 돌아가려면 GDK_EXTENSION_EVENTS_NONE을 쓴다.

이야기는 이것으로 끝나지 않는다. 디폴트로, extension device는 disable 되어 있다. 우리는 사용자로 하여금 그들의 extension device들을 enable시키고 설정할 수 있게 해주는 매커니즘이 필요하다. GTK는 이 과정을 자동적으로 해 주는 InputDialog widget을 제공한다. 다음과 같은 과정대로 InputDialog widget을 관리한다. 이것은 device가 존재하지 않으면 dialog로 알려주고, 나머지 경우엔 top으로 올려보낸다.

void
input_dialog_destroy (GtkWidget *w, gpointer data)
{
    *((GtkWidget **)data) = NULL;
}

void
create_input_dialog ()
{
    static GtkWidget *inputd = NULL;
    if (!inputd)
        {
            inputd = gtk_input_dialog_new();
            gtk_signal_connect (GTK_OBJECT(inputd), "destroy",
                               (GtkSignalFunc)input_dialog_destroy, &inputd);
            gtk_signal_connect_object 
            (GTK_OBJECT(GTK_INPUT_DIALOG(inputd)->close_button),
                     "clicked",
                     (GtkSignalFunc)gtk_widget_hide,
                     GTK_OBJECT(inputd));
            gtk_widget_hide ( GTK_INPUT_DIALOG(inputd)->save_button);
            gtk_widget_show (inputd);
        }
    else
        {
            if (!GTK_WIDGET_MAPPED(inputd))
                gtk_widget_show(inputd);
            else
                gdk_window_raise(inputd->window);
        }
}
(우리가 이 dialog를 다루는 방법에 주의를 기울여야 할 것이다. "destroy" 시그널에 연결시켜 두는 것으로, 우리는 dialog가 파괴된 이후 포인터를 dialog 에 남겨두는 실수 - 이것은 Segfault(!)를 부른다 - 를 피할 수 있다.)

InputDialog는 "Close" 그리고 "Save"라는 두 버튼을 가지고 있으며, 디폴트 로는 아무런 동작이 연결되어 있지 않다. 위에서는 "Close" 버튼을 dialog를 감추는데 썼고, "Save" 버튼은 감추어 버렸다. 이 프로그램에서 XInput을 저장 하는 옵션을 갖추지 않았기 때문이다.

Extended device 정보를 이용하기

일단 장치(device)를 enable시켰으면, 우리는 이벤트 구조체의 나머지 필드에 있는 extended device의 정보를 이용할 수 있다. 사실, 이 정보를 이용하는 건 언제나 안전하다. Extended event가 disable되어 있더라도 이들은 적절한 디폴트값을 가지고 있기 때문이다.

한 가지 주의할 것은 gdk_window_get_pointer() 대신에 gdk_input_window_get_pointer()를 호출한다는 것이다. 이것은 gdk_window_get_pointer가 extended device 정보를 리턴하지 못하기 때문이다.

void gdk_input_window_get_pointer     (GdkWindow       *window,
                                     guint32         deviceid,
                                     gdouble         *x,
                                     gdouble         *y,
                                     gdouble         *pressure,
                                     gdouble         *xtilt,
                                     gdouble         *ytilt,
                                     GdkModifierType *mask);

이 함수를 부를 때, 윈도의 경우와 마찬가지로 device의 ID를 명시해 줘야 한다. 보통, 우리는 device ID를 event 구조체의 deviceid 필드로부터 취할 것이다. 다시 한번, 이 함수는 extension event가 disable된 상태에서도 합당한 값을 리턴함을 기억하자. (이 경우엔, event->deviceidGDK_CORE_POINTER라는 값을 가질 것이다.)

그래서 우리의 button-press 와 motion 이벤트 핸들러의 기본적인 구조는 그리 변하지 않는다 - 우린 단지 extended에 해당하는 정보를 다룰 코드를 추가하기만 하면 된다.

static gint
button_press_event (GtkWidget *widget, GdkEventButton *event)
{
    print_button_press (event->deviceid);
    if (event->button == 1 && pixmap != NULL)
        draw_brush (widget, event->source, event->x, event->y, event->pressure);
    return TRUE;
}

static gint
motion_notify_event (GtkWidget *widget, GdkEventMotion *event)
{
    gdouble x, y;
    gdouble pressure;
    GdkModifierType state;
    if (event->is_hint)
        gdk_input_window_get_pointer (event->window, event->deviceid,
                            &x, &y, &pressure, NULL, NULL, &state);
    else
        {
            x = event->x;
            y = event->y;
            pressure = event->pressure;
            state = event->state;
        }
    if (state & GDK_BUTTON1_MASK && pixmap != NULL)
        draw_brush (widget, event->source, x, y, pressure);
    return TRUE;
}

우린 또한 새로운 정보를 이용하는 일을 뭔가 해야 한다. 우리의 draw_brush() 함수는 각 event->source마다 다른 색깔로 그릴 수 있게 하고, 그리고 pressure에 따라 brush의 크기를 변하게 한다.

/* 스크린에 사각형을 그린다.  크기는 pressure에 의존하고,
 * 그리고 색깔은 device의 타입에 의존한다. */
static void
draw_brush (GtkWidget *widget, GdkInputSource source,
            gdouble x, gdouble y, gdouble pressure)
{
    GdkGC *gc;
    GdkRectangle update_rect;
    switch (source)
        {
        case GDK_SOURCE_MOUSE:
            gc = widget->style->dark_gc[GTK_WIDGET_STATE (widget)];
            break;
        case GDK_SOURCE_PEN:
            gc = widget->style->black_gc;
            break;
        case GDK_SOURCE_ERASER:
            gc = widget->style->white_gc;
            break;
        default:
            gc = widget->style->light_gc[GTK_WIDGET_STATE (widget)];
        }
    update_rect.x = x - 10 * pressure;
    update_rect.y = y - 10 * pressure;
    update_rect.width = 20 * pressure;
    update_rect.height = 20 * pressure;
    gdk_draw_rectangle (pixmap, gc, TRUE,
                        update_rect.x, update_rect.y,
                        update_rect.width, update_rect.height);
    gtk_widget_draw (widget, &update_rect);
}

Device에 대해 더 많은 걸 알아내기

Device에 대한 정보를 어떻게 더 알아내는지 보여주는 예제로서, 우리 프로 그램은 각 버튼을 누르면 device의 이름을 프린트할 것이다. Device의 이름을 알아내기 위하여 이 함수를 이용한다.

GList *gdk_input_list_devices               (void);

이것은 GdkDeviceInfo 구조체의 GList(glib 라이브러리에서 온 연결리스트 타입)를 리턴한다. GdkDeviceInfo 구조체는 이렇게 정의되어 있다.

struct _GdkDeviceInfo
{
        guint32 deviceid;
        gchar *name;
        GdkInputSource source;
        GdkInputMode mode;
        gint has_cursor;
        gint num_axes;
        GdkAxisUse *axes;
        gint num_keys;
        GdkDeviceKey *keys;
};

여기있는 대부분의 필드는 여러분이 XInput 설정을 저장하는 기능을 추가하지 않았다면 무시해도 괜찮은 설정 정보들이다. 지금 우리가 관심을 가진 것은 단순히 X가 device에 부여한 이름(name)이다. 설정(configuration)필드에 해당하지 않는 것은 has_cursor이다. 그런데 우리는 GDK_EXTENSION_EVENTS_CURSOR를 설정했으므로, 이 필드에 대해서는 걱정하지 말자.

print_button_press() 함수는 매치되는 리스트를 리턴할 때까지 단순히 반복될 것이며, device의 이름을 프린트해 줄 것이다.

static void
print_button_press (guint32 deviceid)
{
        GList *tmp_list;
        /* gdk_input_list_devices는 내부적인 리스트를 리턴하며,
         * 따라서 우리는 후에 이것을 해제해 주지 않을 것이다. */
        tmp_list = gdk_input_list_devices();
        while (tmp_list)
                {
                        GdkDeviceInfo *info = (GdkDeviceInfo *)tmp_list->data;
                        if (info->deviceid == deviceid)
                                {
                                        printf("Button press on device '%s'\n", info->name);
                                        return;
                                }
                        tmp_list = tmp_list->next;
                }
}

이것으로 우리의 ``XInputize'' 프로그램을 완전히 변경했다. 첫 버젼과 마찬가지로 이것의 완전한 소스 코드는 이 문서를 구한 곳이나 다음 위치에서 구할 수 있다.

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

더 복잡한 이용

비록 우리 프로그램이 이제 XInput을 꽤 잘 지원함에도 불구하고, 어플 전체적 으로 봐서 뭔가 부족해 보이는 면도 있다. 먼저, 사용자는 프로그램을 실행할 때마다 그들의 장치들을 설정하려고 하진 않을 것이므로, 우리는 그 장치들의 설정을 저장할 수 있도록 해줘야 한다. 이것은 gdk_input_list_devices()의 리턴을 되풀이하고, 설정을 파일에 기록(write)하는 것으로 이루어진다.

프로그램이 다음 번 실행될 때 현재의 state를 복구해 주기 위해서, GDK는 device 설정에 대한 다음 함수들을 제공한다.

gdk_input_set_extension_events()
gdk_input_set_source()
gdk_input_set_mode()
gdk_input_set_axes()
gdk_input_set_key()

(gdk_input_list_devices()가 리턴한 리스트는 정확히 변경되지 않을 수 있다.) 이것에 대한 예제는 그리기 프로그램인 Gsumi( http://www.msc.cornell.edu/~otaylor/gsumi/에 있다.)가 될 것이다. 결국, 모든 어플들에 대해 이 목적으로 표준적인 방식을 가질 수 있다면 멋진 일이 될 것이다. 이것은 아마 GNOME 라이브러리가 GTK보다 약간 높은 레벨에 속할 것이다.

우리가 앞에서 다룬 것에서 빠진 또하나 중요한 것은 cursor에 대한 것이다. XFree86과 다른 플랫폼들은 현재로서는 동시에 둘을 - Core pointer, 그리고 어플에 의한 직접적인 조작 - 사용하여 장치를 다룰 수 없다. 이것에 대해 더 자세한 정보는 XInput-HOWTO를 보라. 이것은 최대한의 이용자를 확보하려는 어플이라면 자신만의 커서를 그려주는 게 필요함을 의미한다.

자신만의 커서를 그리는 어플은 다음의 두 일을 해야 한다. 첫째로 현재의 장치가 그려진 커서를 필요로 하는지의 여부를 결정하고, 둘째로 현재의 장치가 접근(proximity)되어 있는지의 여부를 결정한다. (현재의 장치가 drawing tablet일 경우, stylus가 tablet에서 떨어졌을 때 커서가 보이지 않게 하는 것이 좋을 것이다. 장치가 stylus와 붙어 있을 때, 이를 접근되었다(in proximity)고 표현한다.) 첫번째 작업은 우리가 device의 이름을 알아내기 위해 했듯이, device의 리스트를 조사 함으로써 해결된다. 두번째 작업은 "proximity_out" 이벤트를 설정함으로써 해결한다. 각자의 커서를 그리는 예제는 GTK 배포판에 있는 'testinput' 프로 그램에서 찾을 수 있을 것이다.


다음 이전 차례