FoundationsofGTKDevelopment:Chapter 12

From 흡혈양파의 번역工房
Revision as of 07:02, 29 April 2014 by Onionmixer (talk | contribs) (GTKD 제 12 장 기타 GTK+ 위젯들 페이지 추가)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search
제 12 장 기타 GTK+ 위젯들

기타 GTK+ 위젯들

본 서적에서 가르치고자 하는 내용은 대부분 마쳤다. 하지만 앞에서 다룬 주제에 해당하지 않는 위젯들도 아직 많다. 따라서 이번 장에서는 그러한 위젯들을 다루고자 한다.


가장 먼저 소개할 두 개의 위젯은 그리기에 사용되는 GtkDrawingArea와 GtkLayout이다. 이 두 위젯은 매우 비슷하지만 GtkLayout 위젯의 경우 임의의 위젯을 자체로 포함(embed)시키도록 허용함과 동시 그리기용 함수를 이용한다는 점에서 차이가 있다.


또 자동 완성과 캘린더를 지원하는 GtkEntry에 대해서도 배워볼 참이다. 마지막으로 상태 아이콘, 인쇄 지원, 최근 파일 관리자를 포함해 GTK+ 2.10에 추가된 위젯을 소개하겠다.


이번 장에서는 다음과 같은 내용을 학습할 것이다.

  • 그리기 위젯 GtkDrawingArea와 GtkLayout을 사용하는 방법
  • GtkCalendar를 이용해 연(year)의 월(months)에 대한 정보를 추적하는 방법
  • 최근 파일 추적, 인쇄 지원, 상태 아이콘을 제공하는 GTK+ 2.10에 도입된 위젯의 사용 방법
  • GtkEntryCompletion 객체를 적용함으로써 GtkEntry 위젯에서 자동 완성을 구현하는 방법


그리기 위젯

앞 장에서 GdkWindow에 도형과 텍스트를 그릴 수 있도록 해주는 GdkDrawable 객체에 대해 학습한 바 있다. GTK+는 GtkDrawingArea 위젯을 제공하는데, 이는 당신이 그릴 수 있는 빈 슬레이트에 불과하다.


GtkDrawingArea에서는 아직 사용이 가능한(nondeprecated) 함수로 gtk_drawing_area_new()라는 함수 하나만 제공하는데, 이는 매개변수를 수락하지 않고 새로운 그리기 영역 위젯을 리턴한다.

GtkWidget* gtk_drawing_area_new ();


위젯을 이용하기 위해서는 앞 장에서 위젯의 GdkWindow에 그릴 때 이용했던 함수를 이용하면 된다. GdkWindow 객체는 GdkDrawable 객체가 되기도 한다는 사실을 기억한다.


GdkDrawingArea는 GtkWidget에서 파생되어 GDK 이벤트로 연결될 수 있다는 한 가지 장점이 있다. 개발자는 그리기 영역으로 많은 이벤트를 연결하길 원할 것이다. 가장 먼저 realize로 연결해야 위젯이 인스턴스화될 때 GDK 자원의 생성 등 실행해야 하는 작업을 처리할 수 있을 것이다. configure-event 시그널은 위젯의 크기 변경을 처리해야 할 때를 알려줄 것이다. 또 expose-event 는 이전에 숨겨져 있던 부분이 노출되면 위젯을 다시 그리도록 해준다. expose-event 시그널이 특히 중요한데, 그리기 영역 내용이 expose-event 콜백에 걸쳐 지속되길 원한다면 그 내용을 다시 그려야 하기 때문이다. 마지막으로 버튼과 마우스 클릭 이벤트를 연결하여 사용자와 위젯의 상호작용이 가능하게 할 수 있다.


Gtkd note.png 특정 타입의 이벤트를 수신하기 위해서는 gtk_widget_add_events()가 지원하는 위젯 이벤트의 리스트에 추가해야 한다. 또 사용자로부터 키보드 입력을 수신하기 위해서는 GTK_CAN_FOCUS 플래그를 설정해야 하는데, 포커스가 있는 위젯만 키 누름을 감지할 수 있기 때문이다.


그리기 영역의 예제

리스팅 12-1은 GtkDrawingArea 위젯을 이용해 간단한 그리기 프로그램을 구현한다. 사용자가 버튼을 클릭히고 버튼을 누른 상태에서 포인터를 드래그하면 화면에 점이 그려질 것이다. 이러한 애플리케이션의 스크린샷은 그림 12-1에서 확인할 수 있다.

그림 12-1. 그리기 영역 위젯에 마우스로 텍스트를 그린 모습


그리기 영역의 GdkWindow 객체에서 현재 내용은 사용자가 Delete 키를 누르면 삭제된다. 매우 간단한 프로그램이지만 GtkDrawingArea 위젯과 상호작용하는 방법을 비롯해 이 위젯과 함께 이벤트를 이용하는 방법을 보여준다.


리스팅 12-1. 간단한 그리기 프로그램 (drawingareas.c)

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

static gboolean button_pressed (GtkWidget*, GdkEventButton*, GPtrArray*);
static gboolean motion_notify (GtkWidget*, GdkEventMotion*, GPtrArray*);
static gboolean key_pressed (GtkWidget*, GdkEventKey*, GPtrArray*);
static gboolean expose_event (GtkWidget*, GdkEventExpose*, GPtrArray*);

int main (int argc,
        char *argv[])
{
    GtkWidget *window, *area;
    GPtrArray *parray;

    gtk_init (&argc, &argv);

    window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
    gtk_window_set_title (GTK_WINDOW (window), "Drawing Areas");
    gtk_widget_set_size_request (window, 400, 300);

    g_signal_connect (G_OBJECT (window), "destroy",
            G_CALLBACK (gtk_main_quit), NULL);

    /* Create a pointer array to hold image data. Then, add event masks to the new
    * drawing area widget. */
    parray = g_ptr_array_sized_new (5000);
    area = gtk_drawing_area_new ();
    GTK_WIDGET_SET_FLAGS (area, GTK_CAN_FOCUS);
    gtk_widget_add_events (area, GDK_BUTTON_PRESS_MASK |
            GDK_BUTTON_MOTION_MASK |
            GDK_KEY_PRESS_MASK);

    g_signal_connect (G_OBJECT (area), "button_press_event",
            G_CALLBACK (button_pressed), parray);
    g_signal_connect (G_OBJECT (area), "motion_notify_event",
            G_CALLBACK (motion_notify), parray);
    g_signal_connect (G_OBJECT (area), "key_press_event",
            G_CALLBACK (key_pressed), parray);
    g_signal_connect (G_OBJECT (area), "expose_event",
            G_CALLBACK (expose_event), parray);

    gtk_container_add (GTK_CONTAINER (window), area);
    gtk_widget_show_all (window);

    /* You must do this after the widget is visible because it must first
    * be realized for the GdkWindow to be valid! */
    gdk_window_set_cursor (area->window, gdk_cursor_new (GDK_PENCIL));

    gtk_main ();
    return 0;
}

/* Redraw all of the points when an expose-event occurs. If you do not do this,
* the drawing area will be cleared. */
static gboolean
expose_event (GtkWidget *area,
        GdkEventExpose *event,
        GPtrArray *parray)
{
    guint i, x, y;
    GdkPoint points[5];

    /* Loop through the coordinates, redrawing them onto the drawing area. */
    for (i = 0; i < parray->len; i = i + 2)
    {
        x = GPOINTER_TO_INT (parray->pdata[i]);
        y = GPOINTER_TO_INT (parray->pdata[i+1]);

        points[0].x = x; points[0].y = y;
        points[1].x = x+1; points[1].y = y;
        points[2].x = x-1; points[2].y = y;
        points[3].x = x; points[3].y = y+1;
        points[4].x = x; points[4].y = y-1;

        gdk_draw_points (area->window,
                area->style->fg_gc[GTK_WIDGET_STATE (area)],
                points, 5);
    }

    return TRUE;
}

/* Draw a point where the user clicked the mouse and points on each of the
* four sides of that point. */
static gboolean
button_pressed (GtkWidget *area,
        GdkEventButton *event,
        GPtrArray *parray)
{
    gint x = event->x, y = event->y;
    GdkPoint points[5] = { {x,y}, {x+1,y}, {x-1,y}, {x,y+1}, {x,y-1} };

    gdk_draw_points (area->window,
            area->style->fg_gc[GTK_WIDGET_STATE (area)],
            points, 5);

    g_ptr_array_add (parray, GINT_TO_POINTER (x));
    g_ptr_array_add (parray, GINT_TO_POINTER (y));

    return FALSE;
}

/* Draw a point where the moved the mouse pointer while a button was
* clicked along with points on each of the four sides of that point. */
static gboolean
motion_notify (GtkWidget *area,
        GdkEventMotion *event,
        GPtrArray *parray)
{
    gint x = event->x, y = event->y;
    GdkPoint points[5] = { {x,y}, {x+1,y}, {x-1,y}, {x,y+1}, {x,y-1} };

    gdk_draw_points (area->window,
            area->style->fg_gc[GTK_WIDGET_STATE (area)],
            points, 5);

    g_ptr_array_add (parray, GINT_TO_POINTER (x));
    g_ptr_array_add (parray, GINT_TO_POINTER (y));

    return FALSE;
}

/* Clear the drawing area when the user presses the Delete key. */
static gboolean

key_pressed (GtkWidget *area,
        GdkEventKey *event,
        GPtrArray *parray)
{
    if (event->keyval == GDK_Delete)
    {
        gdk_window_clear (area->window);
        g_ptr_array_remove_range (parray, 0, parray->len);
    }

    return FALSE;
}


리스팅 12-1에서 몇 가지 눈에 띄는 점이 발견될 것이다. 먼저 그리기 영역에 추가된 점을 추적하기 위해 GPtrArray가 사용되었다. 점이 화면에 추가되면 처음 점의 네 면에 있는 점이 모두 활성화된다. 이후 수평 및 수직 위치가 배열로 추가된다. expose-event 콜백 함수가 호출되면 점들이 모두 다시 그려진다. 그리기 영역의 내용을 개발자가 다시 그리지 않으면 expose-event 발생 중에 삭제될 것이다.


점을 그리기 위해 이 애플리케이션은 gdk_draw_points()를 이용한다. 이 함수는 그리기 가능한 객체에 npoints 점의 배열을 그린다. 이는 현재 위젯의 상태에 대한 기본 전경색을 이용한다.

void gdk_draw_points (GdkDrawable *drawable,
        GdkGC *gc,
        GdkPoint *points,
        gint npoints);


gdk_draw_points() 외에도 제 11장에 열거된 그리기 함수 중 어느 것이든 그리기 영역과 함께 이용할 수 있다.


레이아웃 위젯

GtkDrawingArea 외에도 GTK+는 GtkLayout이라는 그리기 위젯을 제공한다. 이 위젯은 사실상 컨테이너로서, 그리기 요소(drawing primitive) 뿐만 아니라 자식 위젯도 지원한다는 점에서 GdkDrawingArea와 다르다. 게다가 GtkLayout은 본래부터 스크롤링 지원을 제공하므로 스크롤이 있는 창에 추가 시 뷰포트가 필요 없다.


Gtkd note.png 레이아웃과 관련해 한 가지 주목해야 할 차이점은 GtkWidget의 window 대신 GtkLayout의 bin_window member로 그려야 한다는 점이다. 예를 들자면, GTK_WIDGET(layout)->window 대신 GTK_LAYOUT(layout)->bin_window로 그려야 할 것이다. 이런 경우 위젯에 자식 위젯을 올바로 포함하도록 해준다.


새로운 GtkLayout 위젯은 gtk_layout_new()를 이용해 생성되는데, 이 함수는 수평 조정과 수직 조정을 수락한다. 조정(adjustment)은 개발자가 두 가지 함수 매개변수로 NULL을 전달하면 알아서 생성될 것이다. GtkLayout에는 네이티브 스크롤링 지원이 있기 때문에 스크롤이 있는 창과 함께 이용 시 GtkDrawingArea를 이용하는 것보다 훨씬 유용할 것이다.

GtkWidget* gtk_layout_new (GtkAdjustment *hadjustment,
        GtkAdjustment *vadjustment);


하지만 GtkLayout은 위젯도 포함할 수 있기 때문에 오버헤드가 어느 정도 부가된다. 이 때문에 위젯의 GdkWindow에만 그려야 한다면 GtkDrawingArea가 더 나은 선택이 되겠다.


자식 위젯은 gtk_layout_put()을 이용해 GtkLayout 컨테이너로 추가되는데, 이 함수를 호출하면 컨테이너의 상단 좌측 모서리를 기준으로 자식 위젯을 위치시킬 것이다. GtkLayout은 GtkContainer에서 직접 상속되기 때문에 다수의 자식을 지원할 수 있다.

void gtk_layout_put (GtkLayout *layout,
        GtkWidget *child_widget,
        gint x,
        gint y);


gtk_layout_move()의 호출은 후에 자식 위젯을 GtkLayout 컨테이너에서 다른 위치로 이동시킬 때 사용할 수 있다.


Gtkd caution.png 개발자는 자식 위젯을 구체적인 수평 및 수직 위치에 두기 때문에 GtkLayout은 GtkFixed와 동일한 문제를 제시한다. 레이아웃 위젯을 이용 시에는 이러한 문제를 주의해야 한다! GtkFixed 위젯에서 발생하는 문제에 대해서는 제 3장을 참고한다.


마지막으로 레이아웃의 크기를 강제로 지정하고 싶다면 새로운 너비와 높이 매개변수를 gtk_layout_set_size()로 전송할 수 있다. gtk_widget_set_size_request() 대신 gtk_layout_set_size()를 이용해야 하는데, 그래야만 조정 매개변수 또한 조정하기 때문이다.

void gtk_layout_set_size (GtkLayout *layout,
        guint width,
        guint height);


뿐만 아니라 크기 요청과 달리 레이아웃 크기조정 함수는 부호가 없는 숫자를 필요로 한다. 즉, 레이아웃 위젯에 대한 절대적인 크기를 명시해야 한다는 의미다. 이 크기는 레이아웃의 총 크기로, 스크롤 영역의 범위를 벗어나 화면에 표시되지 않는 위젯의 부분도 포함해야 한다! GtkLayout 위젯의 기본 크기는 100x100 픽셀이다.


캘린더

GTK+는 특정 월(month)을 표시하는 GtkCalendar 위젯도 제공한다. 이 위젯은 사용자가 그림 12-2에 표시된 바와 같이 스크롤 화살표를 이용해 월과 연도를 움직이도록 해준다. 선택한 연도에 대한 주(week)의 개수와 요일명을 세 자리 문자로 줄여 표시할 수도 있다.

그림 12-2. GtkCalendar 위젯


GtkCalendar 구조체에는 이용 가능한 member의 수가 많지만 모두 읽기만 가능한데, 이러한 객체들을 설명하도록 하겠다. 현재 월과 연도가 프로그램적으로 또는 사용자에 의해 변경되면 이러한 값들은 모두 리셋될 것이란 점을 명심해야 한다. 따라서 아래와 같은 변경내용을 처리해야 할 것이다.

  • num_marked_dates: 당월에 표시된 일수. 이 값은 0과 당월의 총 일자수 사이 값이어야 한다.
  • marked_date: 당월에 표시된 num_marked_dates 일수를 포함하는 부호가 없는 정수의 배열.
  • month: 사용자가 보고 있는 현재 월. 월의 값은 0부터 11 사이의 값이어야 한다. 월이 변경되면 month-changed 시그널이 발생할 것이다. 캘린더가 다음 월이나 이전 월로 이동하면 next-month 또는 previous-month 시그널이 발생할 것이다.
  • year: 표시된 월에 해당하는 현재 연도. 캘린더가 다음 연도 또는 이전 연도로 이동하면 next-year 또는 previous-year 시그널이 발생할 것이다.
  • selected_day: 현재 선택된 일자로, 하나 이상의 일자를 표시할 수는 있지만 항상 하나의 일자에 해당한다. 일자는 1부터 당월에 포함된 일자 수 사이의 값이어야 한다.


새로운 GtkCalendar 위젯은 gtk_calendar_new()를 이용해 생성된다. 기본적으로 현재 일자가 선택된다. 따라서 컴퓨터가 저장한 현재 월과 연도도 표시될 것이다. 선택된 일자는 gtk_calendar_get_date()를 이용해 검색하고, gtk_calendar_select_day()를 이용하면 새로운 일자를 선택할 수 있다. 현재 선택한 일자를 선택 해제하려면 gtk_calendar_select_day()를 이용해 일자(date) 값을 0으로 한다.


GtkCalendar 위젯이 어떻게 표시되는지와 어떻게 사용자와 상호작용하는지를 맞춤설정하기 위해서는 gtk_calendar_set_display_options()를 이용해 GtkCalendarDisplayOptions 값의 bitwise 리스트를 설정해야 한다. 이 열거에서 아직 사용되고 있는 값은 다음과 같다.

  • GTK_CALENDAR_SHOW_HEADING: 설정 시 월과 연도의 이름이 표시될 것이다.
  • GTK_CALENDAR_SHOW_DAY_NAMES: 설정 시 각 일자에 대한 3자리 문자로 된 축약어가 해당 일자의 열 위에 표시될 것이다. 주요 캘린더 내용과 헤더 사이에서 렌더링된다.
  • GTK_CALENDAR_NO_MONTH_CHANGE: 사용자가 캘린더의 현재 월을 변경하지 못하도록 한다. 이 플래그를 설정하지 않으면 다음 또는 이전 월로 이동하도록 화살표가 표시될 것이다. 기본적으로 화살표는 활성화된다.
  • GTK_CALENDAR_SHOW_WEEK_NUMBERS: 현재 연도에 해당하는 캘린더의 좌측을 따라 주(week) 번호를 표시한다. 기본적으로 주 번호는 숨겨진다.


하나의 일자를 선택하는 기능 외에도 gtk_calendar_mark_day()를 이용해 각 월에서 원하는 수만큼 일자를 한 번에 하나씩 선택할 수 있다. 일자가 성공적으로 표시되면 TRUE를 리턴할 것이다.

gboolean gtk_calendar_mark_day (GtkCalendar *calendar,
        guint day);


마크(mark)는 어떤 월에서 연관된 이벤트를 가진 일자를 모두 선택하는 경우 등 여러 용도를 갖고 있다. 일자를 표시하고 나면 일자는 marked_data 배열로 추가될 것이다. 일자를 표시하고 나면 gtk_calendar_unmark_day()를 이용해 표시를 해제할 수도 있으며, 일자가 성공적으로 표시 해제되면 TRUE를 리턴할 것이다. gtk_calendar_clear_marks()를 이용하면 모든 일자의 표시를 해제할 수 있다.

gboolean gtk_calendar_unmark_day (GtkCalendar *calendar,
        guint day);


사용자가 일자를 선택할 때를 감지 시 이용할 수 있는 시그널로는 두 가지가 있다. 첫 번째 시그널인 day-selected는 사용자가 마우스나 키보드로 새로운 일자를 선택하면 발생할 것이다. day-selected-double-click 시그널은 사용자가 더블 클릭을 통해 일자를 선택하면 발생할 것이다. 즉, 대부분의 경우 GtkCalendar 위젯과 함께 button-press-event 시그널이 필요한 일은 없을 것이란 의미다.


상태 아이콘

GtkStatusIcon 위젯은 GTK+ 2.10에서 소개되었고, 플랫폼 독립적인 방식으로 시스템 트레이(알림 영역)에 아이콘을 표시하는 데에 사용된다. 시스템 트레이 아이콘은 종종 일부 타입의 이벤트를 사용자에게 비개입적인 방식으로 알리거나 최소화된 애플리케이션으로 쉽게 접근할 수 있도록 제공된다.


시스템 트레이 아이콘의 GtkStatusIcon 구현은 툴팁을 추가하고, 아이콘과 상호작용을 위한 팝업 메뉴를 추가하며, 사용자에게 특정한 이벤트 타입을 알리기 위해 아이콘을 깜빡이도록 만드는 기능을 제공한다. 사용자가 아이콘을 클릭하여 아이콘을 활성화시키는 것도 가능하다.


Gtkd note.png GtkStatusIcon은 GtkWidget에서 상속되는 것이 아니라 사실상 GOject에 해당한다! Microsoft Windows에서는 시스템 트레이 아이콘은 위젯으로 추가될 수 없기 때문에 꼭 필요하다.


새로운 상태 아이콘을 생성하도록 다섯 가지의 함수가 제공된다. 빈 GtkStatusIcon 인스턴스는 gtk_status_icon_new()를 이용해 생성된다. 해당 초기화 함수를 사용할 경우 객체를 visible로 설정하기 전에 시스템 트레이 아이콘에 해당하는 이미지를 명시해야 한다.

GtkStatusIcon* gtk_status_icon_new ();
GtkStatusIcon* gtk_status_icon_new_from_pixbuf (GdkPixbuf *pixbuf);
GtkStatusIcon* gtk_status_icon_new_from_file (const gchar *filename);
GtkStatusIcon* gtk_status_icon_new_from_stock (const gchar *stock_id);
GtkStatusIcon* gtk_status_icon_new_from_icon_name (const gchar *icon_name);


나머지 네 개의 함수는 각각 GdkPixbuf 객체, 시스템 내 파일, 스톡 항목, 혹은 현재 아이콘 테마의 이미지로부터 상태 아이콘을 생성한다. 이러한 함수들은 모두 필요 시 알림 영역에 맞도록 이미지를 스케일링할 것이다.


gtk_status_icon_new()를 이용해 상태 아이콘을 초기화했다면 gtk_status_icon_set_from_pixbuf()와 그 friends를 이용해 이미지를 설정할 수 있다. GdkPixbuf 객체, 파일, 스톡 항목, 현재 아이콘 테마의 이미지로부터 이미지를 설정하기 위한 함수들이 제공된다. 이러한 함수들은 후에 애플리케이션의 현재 상태를 반영하도록 이미지를 변경할 때에도 사용 가능하다. 가령 애플리케이션이 이메일 클라이언트라면 시스템 트레이 아이콘을 애플리케이션의 아이콘에서 봉투모양(envelop)으로 변경하여 새 메시지가 도착했음을 표시하도록 설정할 수 있겠다.


Gtkd tip.png 기본적으로 상태 아이콘은 visible로 설정된다. gtk_status_icon_set_visible()을 이용해 뷰에서 아이콘을 숨기거나 visible로 설정 가능하다.


사용자가 시스템 트레이 아이콘 위에서 마우스를 움직이면 추가 정보를 제공하는 툴팁을 표시하고자 할 경우에는 gtk_status_icon_set_tooltip()을 이용한다. 예를 들어, 다운로드하는 애플리케이션에서 진행 경과의 퍼센트 또는 이메일 클라이언트에서 새로운 메시지의 개수 등의 정보를 제공할 수 있겠다.

void gtk_status_icon_set_tooltip (GtkStatusIcon *icon,
        const gchar *tooltip_text);


사용자가 알아야 하는 이벤트가 애플리케이션에서 발생하면 gtk_status_icon_set_blinking()을 이용해 상태 아이콘을 깜빡이도록 만들 수 있다. 이 기능은 사용자의 개인설정에 따라 비활성화할 수도 있다. 이런 경우 함수는 어떤 효과도 보이지 않을 것이다. 이 함수를 이용할 때는 깜빡임 기능을 끌 것을 잊지 말라! 깜빡임 기능이 더 이상 필요하지 않은데도 끄지 않을 시 어떤 사람들에게는 애플리케이션의 사용을 포기할 만큼 큰 골칫거리가 되기도 한다.

void gtk_status_icon_set_blinking (GtkStatusIcon *icon,
        gboolean blinking);


GtkStatusIcon은 세 개의 시그널을 제공한다. activate 시그널은 사용자가 상태 아이콘을 활성화하면 발생한다. size-changed 시그널은 아이콘에 이용 가능한 크기가 변경되면 발생한다. 이 덕분에 개발자는 아이콘의 크기를 변경하거나 새로운 아이콘이 새로운 크기에 맞도록 로딩할 수 있는데, 이런 경우 TRUE를 리턴해야 한다. FALSE를 리턴하면 GTK+는 현재 아이콘을 새로운 크기에 맞도록 스케일링할 것이다.


마지막으로 popup-menu 시그널은 사용자가 메뉴를 표시해야 한다고 나타내면 발생한다. 주로 아이콘을 오른쪽 마우스로 클릭하면 되는데, 사용자 플랫폼에 따라 다르다. 이 함수는 각각 어떤 버튼을 눌러야 하는지, 언제 버튼이 활성화되는지를 나타내는 두 개의 부호가 없는 정수를 수락한다. 메뉴를 표시하려면 이 두 개의 값은 gtk_menu_popup()으로 전송되어야 한다. gtk_menu_popup()의 네 번째 매개변수에 대해서는 gtk_status_icon_position_menu()를 사용하길 원할 것이다. 이는 메뉴 위치지정 함수로서 메뉴를 화면 어디에 위치시킬 것인지를 계산할 것이다.


인쇄 지원

GTK+ 2.10에는 라이브러리에 인쇄 지원을 추가하기 위해 다수의 새로운 위젯과 객체가 도입되었다. 대부분의 경우에는 해당 API에 많은 객체가 포함되어 있기 때문에 여러 플랫폼에 걸쳐 사용이 가능한 고수준 인쇄 API인 GtkPrintOperation과 직접적으로 상호작용만 하면 된다. 이는 대부분의 인쇄 연산을 처리하는 데에 있어 사용자가 직접 이용하는(front-end) 인터페이스 역할을 한다.


이번 절에서는 사용자가 GtkFileChoserButton 위젯에서 선택하는 텍스트 파일의 내용을 인쇄하는 애플리케이션을 구현할 것이다. Linux 시스템에 표시되는 기본 인쇄 대화상자의 스크린샷을 그림 12-3에 소개하겠다. 사용자는 GtkFileChooserButton 위젯을 이용해 디스크에서 파일을 선택한 후 메인 메뉴의 Print 버튼을 클릭하여 대화상자를 열 수 있다.

그림 12-3. Linux 시스템의 인쇄 대화상자


리스팅 12-2는 애플리케이션에 필요한 데이터 구조체를 정의하고 사용자 인터페이스를 준비시킴으로써 시작한다. 최종 결과물의 렌더링에 도움이 되는 현재 인쇄 작업에 관한 정보를 보유하기 위해 PrintData 구조체를 이용할 것이다. Widgets는 콜백 함수에서 인쇄 작업의 정보와 여러 개의 위젯으로 접근성을 제공하는 간단한 구조체에 해당한다.


리스팅 12-2. GTK+ 인쇄 예제 (printing.c)

#include <gtk/gtk.h>
#include <math.h>

#define HEADER_HEIGHT 20.0
#define HEADER_GAP 8.5

/* A structure that will hold information about the current print job. */
typedef struct
{
    gchar *filename;
    gdouble font_size;
    gint lines_per_page;
    gchar **lines;
    gint total_lines;
    gint total_pages;
} PrintData;

typedef struct
{
    GtkWidget *window, *chooser;
    PrintData *data;
} Widgets;

GtkPrintSettings *settings;

static void print_file (GtkButton*, Widgets*);
static void begin_print (GtkPrintOperation*, GtkPrintContext*, Widgets*);
static void draw_page (GtkPrintOperation*, GtkPrintContext*, gint, Widgets*);
static void end_print (GtkPrintOperation*, GtkPrintContext*, Widgets*);

int main (int argc,
        char *argv[])
{
    GtkWidget *hbox, *print;
    Widgets *w;

    gtk_init (&argc, &argv);

    w = g_slice_new (Widgets);
    w->window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
    gtk_window_set_title (GTK_WINDOW (w->window), "Printing");
    gtk_container_set_border_width (GTK_CONTAINER (w->window), 10);

    g_signal_connect (G_OBJECT (w->window), "destroy",
            G_CALLBACK (gtk_main_quit), NULL);

    w->chooser = gtk_file_chooser_button_new ("Select a File",
            GTK_FILE_CHOOSER_ACTION_OPEN);
    gtk_file_chooser_set_current_folder (GTK_FILE_CHOOSER (w->chooser),
            g_get_home_dir ());

    print = gtk_button_new_from_stock (GTK_STOCK_PRINT);

    g_signal_connect (G_OBJECT (print), "clicked",
            G_CALLBACK (print_file), (gpointer) w);

    hbox = gtk_hbox_new (FALSE, 5);
    gtk_box_pack_start (GTK_BOX (hbox), w->chooser, FALSE, FALSE, 0);
    gtk_box_pack_start (GTK_BOX (hbox), print, FALSE, FALSE, 0);

    gtk_container_add (GTK_CONTAINER (w->window), hbox);
    gtk_widget_show_all (w->window);

    gtk_main ();
    return 0;
}


리스팅 12-2의 윗부분에서 HEADER_HEIGHT와 HEADER_GAP이라는 두 개의 값이 정의된다. HEADER-HEIGHT는 렌더링될 헤더 텍스트에 이용할 수 있는 공간의 양이다. 이것은 파일명이나 페이지 번호와 같은 정보를 표시하는 데에 사용될 것이다. HEADER_GAP은 헤더와 실제 페이지 내용 사이에 위치할 패딩이다.


현재 인쇄 작업에 관한 정보를 저장하기 위해서는 PrintData 구조체를 이용할 것이다. 이러한 정보로는 디스크 상에서 파일의 위치, 글꼴 크기, 한 페이지에 렌더링 가능한 행의 수, 파일의 내용, 총 행의 수, 총 페이지 수가 포함된다.


인쇄 연산

다음 단계는 GTK_STOCK_PRINT 버튼을 클릭하면 실행될 콜백 함수를 구현하는 일이다. 이 함수는 리스팅 12-3에서 구현하였다. 이는 PrintData 객체를 생성하고, 필요한 시그널을 모두 연결하며, 인쇄 연산을 생성하는 일을 처리할 것이다.


리스팅 12-3. 인쇄와 인쇄 미리보기

/* Print the selected file with a font of "Monospace 10". */
static void
print_file (GtkButton *button,
        Widgets *w)
{
    GtkPrintOperation *operation;
    GtkWidget *dialog;
    GError *error = NULL;
    gchar *filename;
    gint res;

    /* Return if a file has not been selected because there is nothing to print. */
    filename = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (w->chooser));
    if (filename == NULL)
        return;

    /* Create a new print operation, applying saved print settings if they exist. */H
    operation = gtk_print_operation_new ();
    if (settings != NULL)
        gtk_print_operation_set_print_settings (operation, settings);

    w->data = g_slice_new (PrintData);
    w->data->filename = g_strdup (filename);
    w->data->font_size = 10.0;

    g_signal_connect (G_OBJECT (operation), "begin_print",
            G_CALLBACK (begin_print), (gpointer) w);
    g_signal_connect
 (G_OBJECT (operation), "draw_page",
            G_CALLBACK (draw_page), (gpointer) w);
    g_signal_connect (G_OBJECT (operation), "end_print",
            G_CALLBACK (end_print), (gpointer) w);

    /* Run the default print operation that will print the selected file. */
    res = gtk_print_operation_run (operation, GTK_PRINT_OPERATION_ACTION_PRINT_DIALOG,
            GTK_WINDOW (w->window), &error);

    /* If the print operation was accepted, save the new print settings. */
    if (res == GTK_PRINT_OPERATION_RESULT_APPLY)
    {
        if (settings != NULL)
            g_object_unref (settings);
        settings = g_object_ref (gtk_print_operation_get_print_settings (operation));
    }
    /* Otherwise, report that the print operation has failed. */
    else if (error)

    {
        dialog = gtk_message_dialog_new (GTK_WINDOW (w->window),
                GTK_DIALOG_DESTROY_WITH_PARENT,
                GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE,
                error->message);

        g_error_free (error);
        gtk_dialog_run (GTK_DIALOG (dialog));
        gtk_widget_destroy (dialog);
    }

    g_object_unref (operation);
    g_free (filename);
}


인쇄의 첫 번째 단계는 새로운 인쇄 연산을 생성하는 일로, gtk_print_operation_new()를 이용하면 가능하다. GtkPrintOperation이 독특한 이유는 플랫폼의 네이티브 인쇄 대화상자가 있을 경우 그것을 사용하기 때문이다. 그러한 대화상자를 제공하지 않는 UNIX와 같은 플랫폼에서는 GtkPrintUnixDialog가 사용될 것이다.


Gtkd note.png 대부분의 애플리케이션에서는 인쇄(print) 객체와 직접 상호작용하는 대신 가능하면 GtkPrintOperation API를 사용해야 한다. GtkPrintOperation은 플랫폼 독립적인 인쇄 해답으로 생성되어 많은 양의 코드가 없이는 쉽게 구현할 수 없다.


다음 단계는 연산에 인쇄 설정을 적용하도록 gtk_print_operation_print_settings()를 호출해야 한다. 이 애플리케이션에서 GtkPrintSettings 객체는 settings라고 불리는 전역 변수로 저장된다. 인쇄 연산이 성공적으로 실행되면 개발자는 현재 인쇄 설정을 저장해야만 추후 인쇄 작업에 동일한 설정을 적용할 수 있다.


다음으로 g_slice_new()를 이용하면 새로운 객체를 할당하여 PrintData 구조체를 준비할 수 있다. 파일명은 이미 존재가 확인된 GtkFileChooserButton에서 현재 선택된 파일로 설정된다. 인쇄 글꼴 크기 또한 10.0 포인트로 설정된다. 텍스트 편집 애플리케이션에서 이러한 글꼴은 현재 GtkTextView의 글꼴에서 검색하는 것이 보통이다. 좀 더 복잡한 인쇄 애플리케이션에서는 글꼴 크기가 문서마다 다양하지만 이번 예제는 시작 단계의 개발자들을 대상으로 하기 때문에 간단하게 구현하였다.


이제 세 개의 GtkPrintOperation 시그널을 연결해야 하는데, 이에 관해서는 이번 절의 뒷부분에서 상세히 논하겠다. 간략하게 말하자면, begin-print는 페이지가 렌더링되기 전에 호출되며, 페이지 수를 설정하고 필요한 준비작업을 실행하는 데에 사용할 수 있다. draw-page 시그널은 페이지의 렌더링을 가능하게 해주기 때문에 인쇄 작업에 있는 모든 페이지마다 호출된다. 마지막으로 end-print 시그널은 인쇄 연산의 성공이나 실패 여부와 상관없이 인쇄 연산이 완료되고 나면 호출된다. 이러한 콜백 함수는 인쇄 작업이 끝난 후 정리하는 데에 사용된다. 인쇄 연산에 걸쳐 사용 가능한 그 외 시그널도 많이 존재하는데, 전체 리스트는 부록 B에서 찾을 수 있다.


인쇄 연산이 준비되고 나면 gtk_print_operation_run()을 호출하여 인쇄를 시작해야 한다. 이 함수에서는 인쇄 연산이 어떠한 작업을 실행할 것인지 정의한다.

GtkPrintOperationResult gtk_print_operation_run (GtkPrintOperation *operation,
        GtkPrintOperationAction action,
        GtkWindow *parent,
        GError **error);


아래 표시된 GtkPrintOperationAction 열거는 인쇄 연산이 실행하는 인쇄 작업을 정의한다. 문서를 인쇄하기 위해서는 GTK_PRINT_OPERATION_ACTION_PRINT_DIALOG를 이용해야 한다.

  • GTK_PRINT_OPERATION_ACTION_PRINT_DIALOG: 플랫폼의 기본 인쇄 대화상자를 표시하고, 기본 대화상자가 없다면 GtkPrintUnixDialog를 사용한다. 대부분의 인쇄 연산에서 일반적인 액션에 해당한다.
  • GTK_PRINT_OPERATION_ACTION_PRINT: 인쇄 대화상자를 표시하지 않고 현재 인쇄 설정을 이용해 인쇄를 시작한다. 사용자가 이 액션을 승인할 것이라고 100% 확신할 때만 실행해야 한다. 사용자에게 이미 확인 대화상자를 표시한 경우를 예로 들 수 있겠다.
  • GTK_PRINT_OPERATION_ACTION_PREVIEW: 현재 설정으로 실행될 인쇄 작업을 미리보기한다. 인쇄 연산과 동일한 렌더링용 콜백을 사용하므로 준비 및 실행에 약간의 작업이 필요하다.
  • GTK_PRINT_OPERATION_ACTION_EXPORT: 인쇄 작업을 파일로 내보내기(export)한다. 이 설정을 이용하기 위해서는 연산을 실행하기 전에 export-filename 프로퍼티를 설정해야 한다.


gtk_print_operation_run()의 마지막 두 매개변수는 인쇄 대화상자에 사용할 부모 창 또는 GError 구조체를 정의하도록 해주기도 하고, NULL을 이용해 매개변수를 무시할 수도 있다. 이 함수는 모든 페이지가 렌더링되고 프린터로 전송되고 나서야 리턴할 것이다.


함수가 제어를 다시 돌려주면 GtkPrintOperationResult 열거 값을 리턴할 것이다. 이 값은 개발자가 그 다음으로 어떤 작업을 실행해야 하는지, 인쇄 연산이 성공했는지 실패했는지에 대한 설명을 제공한다. 네 가지 열거 값을 아래 소개하겠다.

  • GTK_PRINT_OPERATION_RESULT_ERROR: 인쇄 연산에서 특정 타입의 오류가 발생하였다. 상세한 정보는 GError 객체를 이용해야 한다.
  • GTK_PRINT_OPERATION_RESULT_APPLY: 인쇄 설정이 변경되었다. 따라서 변경 내용을 손실하지 않기 위해서는 즉시 저장해야 한다.
  • GTK_PRINT_OPERATION_RESULT_CANCEL: 사용자가 인쇄 연산을 취소했으며, 개발자는 변경 내용을 인쇄 설정으로 저장해선 안 된다.
  • GTK_PRINT_OPERATION_RESULT_IN_PROGRESS: 인쇄 연산이 아직 완료되지 않았다. 작업을 비동기식으로 실행 중일 때만 이 값을 얻을 것이다.


인쇄 연산을 비동기식으로 실행하는 것도 가능한데, 이는 페이지의 렌더링이 완료되기 전에 gtk_print_operation_run()이 리턴할 수도 있다는 의미다. 이러한 실행은 gtk_print_operation_set_allow_async()를 통해 설정된다. 모든 플랫폼이 이 연산을 허용하는 것은 아니므로 혹시 작동하지 않을 수도 있으니 마음의 준비를 하도록 한다!


인쇄 연산을 비동기식으로 실행할 경우 done 시그널을 이용해 인쇄가 완료되면 알림을 복구시킬 수 있다. 이 시점에서 개발자에게 인쇄 연산 결과가 제공되는데, 결과는 알림에 따라 처리해야 할 것이다.


인쇄 연산의 결과를 처리하고 난 후에 결과적인 오류가 설정되어 존재한다면 오류 또한 처리해야 한다. GtkPrintError 도메인에서 발생 가능한 오류의 리스트는 부록 E에서 찾아볼 수 있다. 인쇄 연산에서 가장 최근에 발생한 GError를 검색하기 위해서는 gtk_print_operation_get_error()를 이용할 수도 있다. 이는 GTK_PRINT_OPERATION_RESULT_ERROR를 리턴한 인쇄 작업에 관한 추가 정보를 검색하기 위해 비동기식으로 인쇄 연산을 실행할 때 사용 가능하다.


GtkPrintOperation이 제공하는 한 가지 독특한 기능으로, 인쇄 연산이 실행되는 동안 진행 대화상자를 표시하는 기능을 들 수 있다. 기본적으로는 꺼져 있지만 gtk_print_operation_set_show_progress()를 이용해 켤 수 있다. 사용자로 하여금 여러 개의 인쇄 연산을 동시에 실행하도록 허용할 때 특히 유용한 기능이다.

void gtk_print_operation_set_show_progress (GtkPrintOperation *operation,
        gboolean show_progress);


때로는 현재 인쇄 작업을 취소해야 하는 경우가 있는데, 이럴 때는 gtk_print_operation_cancel()을 호출하면 된다. 이 함수는 주로 begin-print, paginate, 또는 draw-page 콜백 함수에서 사용된다. 이 함수는 사용자가 활성화된 인쇄 연산 도중에 중단할 수 있도록 Cancel 버튼을 제공하도록 해주기도 한다.

void gtk_print_operation_cancel (GtkPrintOperation *operation);


인쇄 작업에 유일한 이름을 제공할 수도 있는데, 그 이름은 외부 인쇄 모니터링 애플리케이션에서 작업을 식별하는 데에 사용될 것이다. 인쇄 작업의 이름은 gtk_print_operation_set_job_name()을 이용해 주어진다. 이 값이 설정되지 않으면 GTK+는 자동으로 인쇄 작업에 이름을, 연속된 인쇄 작업에는 숫자를 지정한다.


인쇄 작업을 비동기식으로 실행 중이라면 인쇄 작업의 현재 상태를 검색하길 원할 것이다. gtk_print_operation_get_status()를 호출하면 인쇄 작업의 상태에 관한 추가 정보를 제공하는 GtkPrintStatus 열거 값이 리턴될 것이다. 인쇄 작업 상태에 가능한 값의 리스트는 다음과 같다.

  • GTK_PRINT_STATUS_INITIAL: 인쇄 연산이 아직 시작되지 않았다. 이것은 기본 초기값이므로 인쇄 대화상자가 아직 표시되어 있을 때 이 상태가 리턴될 것이다.
  • GTK_PRINT_STATUS_PREPARING: 인쇄 연산이 두 페이지로 나뉘고, begin-print 시그널이 발생하였다.
  • GTK_PRINT_STATUS_GENERATING_DATA: 페이지가 렌더링 중이다. 이 값은 draw-page 시그널이 발생하는 동안 설정될 것이다. 이 때는 어떤 데이터도 프린터로 전송되지 않을 것이다.
  • GTK_PRINT_STATUS_SENDING_DATA: 인쇄 작업에 관한 데이터가 프린터로 전송 중이다.
  • GTK_PRINT_STATUS_PENDING: 모든 데이터가 프린터로 전송되었지만 작업은 처리되지 않았다. 프린터를 중단시키는 것이 가능하다.
  • GTK_PRINT_STATUS_PENDING_ISSUE: 인쇄 도중에 문제가 발생하였다. 프린터에 용지가 부족한 경우 혹은 용지가 걸리는 경우를 예로 들 수 있다.
  • GTK_PRINT_STATUS_PRINTING: 프린터가 현재 인쇄 작업을 처리 중이다.
  • GTK_PRINT_STATUS_FINISHED: 인쇄 작업이 성공적으로 완료되었다.
  • GTK_PRINT_STATUS_FINISHED_ABORTED: 인쇄 작업이 취소되었다. 작업을 다시 실행하지 않는 한 어떤 액션도 취하지 않을 것이다.


gtk_print_operation_get_status()가 리턴한 값은 수치값이기 때문에 애플리케이션 내에서 사용 가능하다. 하지만 GTK+는 gtk_print_operation_get_status_string()을 이용해 문자열을 검색하는 기능도 제공하는데, 이 문자열은 인쇄 작업 상태를 설명하는 사람이 읽을 수 있는 문자열에 해당한다. 이 문자열은 출력의 디버깅 또는 사용자에게 인쇄 작업에 관한 추가 정보를 표시하는 데에 사용할 수 있다. 가령 상태 바 위에 혹은 메시지 대화상자 안에 문자열을 표시할 수 있겠다.


인쇄 연산 시작하기

인쇄 연산이 모두 준비되었으니 필요한 시그널 콜백 함수를 구현할 차례다. begin-print 시그널은 사용자가 인쇄를 시작 시, 즉 사용자의 관점에서 모든 설정이 완료되면 발생한다.


리스팅 12-4에서는 begin_print() 콜백 함수를 이용해 먼저 파일의 내용을 검색하여 여러 개의 행으로 나누었다. 그리고 총 행의 수가 계산되는데, 이는 페이지 수를 검색하는 데에 사용할 수 있다.


리스팅 12-4. begin-print 시그널에 대한 콜백 함수

/* Begin the printing by retrieving the contents of the selected files and
* splitting it into single lines of text. */
static void
begin_print (GtkPrintOperation *operation,
        GtkPrintContext *context,
        Widgets *w)
{
    gchar *contents;
    gdouble height;
    gsize length;

    /* Retrieve the file contents and split it into lines of text. */
    g_file_get_contents (w->data->filename, &contents, &length, NULL);
    w->data->lines = g_strsplit (contents, "\n", 0);

    /* Count the total number of lines in the file. */
    w->data->total_lines = 0;
    while (w->data->lines[w->data->total_lines] != NULL)
        w->data->total_lines++;

    /* Based on the height of the page and font size, calculate how many lines can be
    * rendered on a single page. A padding of 3 is placed between lines as well. */
    height = gtk_print_context_get_height (context) - HEADER_HEIGHT - HEADER_GAP;
    w->data->lines_per_page = floor (height / (w->data->font_size + 3));
    w->data->total_pages = (w->data->total_lines - 1) / w->data->lines_per_page + 1;
    gtk_print_operation_set_n_pages (operation, w->data->total_pages);
    g_free (contents);
}


인쇄 연산에서 필요로 하는 페이지 수를 계산하기 위해서는 모든 페이지에 얼마나 많은 행이 렌더링될 수 있는지 알아내야 한다. 모든 페이지의 총 높이는 gtk_print_context_get_height()를 이용해 검색할 수 있는데, 이는 GtkPrintContext 객체에 저장된다. GtkPrintContext는 페이지를 어떻게 그리는지에 관한 정보를 저장하는 데에 사용된다. 예를 들어 이 객체는 페이지 셋업, 너비와 높이 치수, 양방향으로 인치당 도트 수를 저장한다. draw-page 콜백 함수는 이번 장의 뒷부분에서 상세히 살펴보겠다.


텍스트의 렌더링에 사용될 페이지의 총 높이를 얻었다면 텍스트 글꼴 크기에서 각 행 사이에 3 픽셀의 공간을 더한 값을 높이에서 나누어야 한다. 페이지별 행의 수를 내림(round down)하도록 floor() 함수를 이용하여 전면(full page)의 하단에 클리핑(clipping)이 발생하지 않도록 하였다.


페이지별 행의 수를 얻었다면 이제 총 페이지 수를 계산할 수 있다. 개발자는 이 값을 gtk_print_operation_set_n_pages() 콜백 함수가 끝날 무렵 이 함수로 전송해야 한다. GTK+가 draw-page 콜백 함수를 얼마나 많이 호출해야 하는지 알도록 페이지 수가 사용될 것이다. 페이지 수는 양수 값이어야 하는데, 그래야만 기본값 -1에서 변경되기 전에는 렌더링이 시작되지 않도록 확보할 수 있기 때문이다.


페이지 렌더링하기

그 다음은 렌더링되어야 하는 페이지마다 한 번씩 호출될 draw-page 콜백 함수를 구현할 차례다. 이 콜백 함수를 이용하려면 Cairo라고 불리는 또 다른 라이브러리를 소개할 필요가 있다. Cairo는 벡터 그래픽 라이브러리로서 무엇보다 인쇄 연산을 렌더링하는 데에 사용된다.


리스팅 12-5에서는 가장 먼저 gtk_print_context_get_cairo_context()를 이용해 현재 GtkPrintContext에 대한 Cairo 드로잉 컨텍스트를 검색하였다. cairo_t 객체를 이용해 인쇄 내용을 렌더링한 후 PangoLayout으로 적용하였다.


이 콜백 함수가 시작되면 GtkPrintContext로부터 두 개의 다른 값을 검색해야 한다. 첫 번째는 gtk_print_context_get_widget()로, 문서의 너비를 리턴하는 함수다. 각 페이지에 일치하는 행의 수는 이미 계산했으므로 페이지 높이를 검색할 필요는 없음을 기억한다. 텍스트가 페이지보다 넓으면 클리핑될 것이다. 문서의 클리핑을 피하려면 이 예제를 수정해야 할 것이다.


Gtkd caution.png GtkPrintContext가 리턴한 너비는 픽셀로 되어 있다.다른 함수들은 Pango 단위(unit)나 포인트(point)와 같은 다른 대안적 스케일을 사용할 수 있으므로 주의해야 한다.


다음 단계에서는 인쇄 컨텍스트에 사용 가능한 gtk_print_context_create_layout()을 이용해 PangoLayout을 생성해야 한다. 인쇄 연산에도 이런 방식으로 Pango 레이아웃을 생성해야 하는데, 그 이유는 인쇄 연산에 이미 올바른 글꼴 메트릭스(font metrics)가 적용되어 있기 때문이다.


리스팅 12-5. draw-page 시그널에 대한 콜백 함수

/* Draw the page, which includes a header with the file name and page number along
* with one page of text with a font of "Monospace 10". */
static void
draw_page (GtkPrintOperation *operation,
        GtkPrintContext *context,
        gint page_nr,
        Widgets *w)
{
    cairo_t *cr;
    PangoLayout *layout;
    gdouble width, text_height;
    gint line, i, text_width, layout_height;
    PangoFontDescription *desc;
    gchar *page_str;

    cr = gtk_print_context_get_cairo_context (context);
    width = gtk_print_context_get_width (context);
    layout = gtk_print_context_create_pango_layout (context);
    desc = pango_font_description_from_string ("Monospace");
    pango_font_description_set_size (desc, w->data->font_size * PANGO_SCALE);

    /* Render the page header with the filename and page number. */
    pango_layout_set_font_description (layout, desc);
    pango_layout_set_text (layout, w->data->filename, -1);
    pango_layout_set_width (layout, -1);
    pango_layout_set_alignment (layout, PANGO_ALIGN_LEFT);
    pango_layout_get_size (layout, NULL, &layout_height);
    text_height = (gdouble) layout_height / PANGO_SCALE;

    cairo_move_to (cr, 0, (HEADER_HEIGHT - text_height) / 2);
    pango_cairo_show_layout (cr, layout);

    page_str = g_strdup_printf ("%d of %d", page_nr + 1, w->data->total_pages);
    pango_layout_set_text (layout, page_str, -1);
    pango_layout_get_size (layout, &text_width, NULL);
    pango_layout_set_alignment (layout, PANGO_ALIGN_RIGHT);

    cairo_move_to (cr, width - (text_width / PANGO_SCALE),
            (HEADER_HEIGHT - text_height) / 2);
    pango_cairo_show_layout (cr, layout);

    /* Render the page text with the specified font and size. */
    cairo_move_to (cr, 0, HEADER_HEIGHT + HEADER_GAP);
    line = page_nr * w->data->lines_per_page;
    for (i = 0; i < w->data->lines_per_page && line < w->data->total_lines; i++)
    {
        pango_layout_set_text (layout, w->data->lines[line], -1);
        pango_cairo_show_layout (cr, layout);
        cairo_rel_move_to (cr, 0, w->data->font_size + 3);
        line++;
    }

    g_free (page_str);
    g_object_unref (layout);
    pango_font_description_free (desc);
}


그 다음으로 이 함수는 파일명을 페이지의 상단 좌측 모서리에 추가하는 연산을 실행하였다. 먼저 pango_layout_set_text()는 레이아웃이 저장한 현재 텍스트를 파일명으로 설정한다. 레이아웃의 너비는 -1로 설정되어야만 파일명이 사선(/) 문자에서 래핑(wrap)하지 않는다. 텍스트는 pango_layout_set_alignment()를 이용해 레이아웃의 좌측으로 정렬된다.


텍스트가 레이아웃으로 추가되었으니 cairo_move_to()를 이용해 Cairo에서 현재 포인트를 페이지의 좌측과 헤더의 중앙으로 이동한다. PangoLayout의 높이는 먼저 PANGO_SCALE의 인수(factor)만큼 감소해야 한다!

void cairo_move_to (cairo_t *cairo_context,
        double x,
        double y);


다음으로 Cairo 컨텍스트에 PangoLayout을 그리기 위해 pango_cairo_show_layout()을 호출한다. 레이아웃의 상단 좌측 모서리는 Cairo 컨텍스트의 현재 포인트에서 렌더링된다. 이 때문에 cairo_move_to()를 이용해 바람직한 위치로 먼저 이동해야 했다.

void pango_cairo_show_layout (cairo_t *cairo_context,
        PangoLayout *layout);


파일명을 렌더링한 후에는 동일한 방법을 이용해 페이지 계수를 각 페이지의 상단 우측 모서리로 추가한다. PangoLayout이 리턴한 너비는 PANGO_SCALE로 축소되어야만 다른 Cairo 값들과 동일한 단위가 될 것이라는 사실을 명심한다.


다음으로는 현재 페이지에 대한 모든 행을 렌더링해야 한다. 먼저 페이지 좌측으로 헤더 아래의 HEADER_GAP 단위를 이동시켜야 한다. 그러면 각 행은 pango_cairo_show_layout()을 이용해 Cairo 컨텍스트로 점차적으로(incrementally) 렌더링된다. 한 가지 흥미로운 점은 루프에서 커서의 위치는 cairo_rel_move_to()를 이용해 이동된다는 것이다.

void cairo_rel_move_to (cairo_t *cairo_context),
        double dx,
        double dy);


이 함수는 이전 위치를 기준으로 현재 위치를 이동하는 데에 사용된다. 따라서 한 행이 렌더링되고 나면 현재 위치가 한 줄 밑으로 이동하는데, 글꼴은 Monospace이기 때문에 텍스트의 글꼴 크기와 동일하다.


Gtkd tip.png 이전 위치를 기준으로 커서를 이동시키면 텍스트의 각 행과 주위의 행 사이에 임의의 공간을 추가하기가 쉬운데, 다만 begin-print 콜백 함수에서 페이지 수를 계산할 때 이 추가 높이를 계산에 넣은 경우에 한해서만 해당된다.


GTK+를 이용해 개발할 때는 전체 Cairo 라이브러리를 이용할 수 있다. 이번 장에서 Cairo를 다룬 절에서 몇 가지 기본적인 내용을 소개하겠다. 하지만 자신만의 애플리케이션에서 인쇄를 구현 중이라면 Cairo API 문서에서 라이브러리를 좀 더 학습하도록 한다.


인쇄 연산 최종화하기

모든 페이지를 렌더링했다면 end-print 시그널이 발생할 것이다. 리스팅 12-6은 이 시그널에 사용될 콜백 함수를 보여준다. 이는 PrintData 객체에서 동적으로 할당된 모든 메모리를 해제하고 나서 객체 자체도 해제한다.


리스팅 12-6. end-print 시그널에 대한 콜백 함수

/* Clean up after the printing operation since it is done. */
static void
end_print (GtkPrintOperation *operation,
        GtkPrintContext *context,
        Widgets *w)
{
    g_strfreev (w->data->lines);
    g_slice_free1 (sizeof (PrintData), w->data);
    w->data = NULL;
}


GTK+가 제공하는 인쇄 API는 어차피 크기가 방대하기 때문에 PangoLayout 과 Cairo에 대해 규모가 큰 API를 고려하지 않아도 된다. 따라서 이번 예제는 시작 단계에 불과하며 API가 제시하는 학습 곡선을 수월하게 완료하도록 도와주는 간단한 예제에 불과하다. 자신만의 애플리케이션에 인쇄를 구현할 때 이 예제를 이용해 시작해도 좋지만 대부분의 경우 이 주제를 좀 더 깊게 연구해야 할 것이다.


Cairo 드로잉 컨텍스트

Cairo는 GTK+ 라이브러리에 걸쳐 사용된 그래픽 렌더링 라이브러리다. 본 서적에서는 인쇄 연산 중에 페이지를 렌더링하도록 Cairo를 이용하였다. 이번 절에서는 cairo_t 객체를 비롯해 이와 연관된 몇 가지 그리기 함수를 소개하겠다.


GTK+에서 인쇄 연산의 페이지는 cairo_t 객체로 렌더링된다. 이 객체는 텍스트를 렌더링하고, 다양한 도형과 선(line)을 그리도록 해주며, 클리핑된 영역을 색상으로 채우도록 해준다. Cairo 드로잉 컨텍스트를 조작할 수 있도록 Cairo가 제공하는 함수를 몇 가지 살펴보겠다.


패스(path) 그리기

Cairo 컨텍스트에서 도형은 패스를 이용해 렌더링된다. 새로운 패스는 cairo_new_path()를 이용해 생성된다. 이후 cairo_copy_path()를 통해 새로운 패스의 복사본을 검색하고 패스에 새로운 선과 도형을 추가할 수 있다.

cairo_path_t* cairo_copy_path (cairo_t *cairo_context);


패스를 그리는 데에 제공되는 함수에는 다수가 있는데, 표 12-1에 열거하겠다. 이 함수에 관한 추가 정보는 Cairo API 문서에서 찾을 수 있다.

함수 설명
cairo_arc() 현재 패스에 호(arc)를 그린다. 호의 반경, 중심(center)의 가로와 세로 위치, 곡선의 시작 각도와 끝 각도(라디안)를 제공한다.
cairo_curve_to() 현재 패스에 Bezier 곡선을 생성한다. 개발자는 곡선을 계산하는 데에 사용될 두 개의 제어점과 곡선의 끝 위치를 제공해야 한다.
cairo_line_to() 현재 위치에서 명시된 점까지 직선을 그린다. 첫 지점이 존재하지 않으면 현재 위치는 단순히 이동할 것이다.
cairo_move_to() 컨텍스트 내 새로운 위치로 이동하면 새로운 하위패스가 생성될 것이다.
cairo_rectangle() 현재 패스 내에 직사각형을 그린다. 개발자는 직사각형의 상단 좌측 모서리의 좌표와 직사각형의 너비 및 높이를 제공해야 한다.
cairo_rel_curve_to() 이 함수는 현재 위치를 기준으로 그려진다는 점을 제외하면 cairo_curve_to()와 동일하다.
cairo_rel_line_to() 이 함수는 현재 위치를 기준으로 그려진다는 점을 제외하면 cairo_line_to()와 동일하다.
cairo_rel_move_to() 이 함수는 현재 위치를 기준으로 그려진다는 점을 제외하면 cairo_move_to()와 동일하다.
표 12-1. Cairo 패스 그리기 함수


하위패스의 사용을 완료하면 cairo_path_close()를 이용해 닫아야 한다. 이 함수를 이용하면 필요 시 현재 패스를 색상으로 채울 수 있도록 패스를 에워쌀(enclose) 것이다.


렌더링 옵션

소스에서 그리기 옵션에 사용된 현재 색상은 cairo_set_source_rgb()에 해당한다. 색상은 새로운 색상이 설정될 때까지 사용될 것이다. 색상의 선택 외에도 cairo_set_source_rgba()를 이용하면 다섯 번째 알파 매개변수를 수락한다. 각 색상 매개변수는 0.0과 1.0 사이의 부동 소수점수다.

void cairo_set_source_rgb (cairo_t *cairo_context,
        double red,
        double green,
        double blue);


현재 패스를 구체적인 지점으로 옮겨 소스의 색상을 선택하고 나면 컨텍스트만 수락하는 cairo_fill()을 이용해 패스를 채울 수 있다. 아니면 cairo_fill_extents()를 이용해 직사각형 영역을 채우는 수도 있다. 이 함수는 (x1,y1)과 (x2,y2)의 모서리로 된 영역을 계산하여 두 점 사이의 영역이면서 현재 패스에 의해 포함된 영역을 모두 채운다.

void cairo_fill_extents (cairo_t *cairo_context,
        double *x1,
        double *y1,
        double *x2,
        double *y2);


곡선과 같은 그리기 연산은 도형의 변을 들쑥날쑥하게 만드는 경우가 있다. 이를 수정하기 위해 Cairo는 cairo_set_antialias()를 통해 그리기에 안티 얼레이징(antialiasing)을 제공한다.

void cairo_set_antialias (cairo_t *cairo_context,
        cairo_antialias_t antialias);


안티 얼레이징은 cairo_antialias_t 열거에서 제공된다. 이 열거에서 정의하는 값은 다음과 같다.

  • CAIRO_ANTIALIAS_DEFAULT: 기본 안티 얼레이징 알고리즘이 사용될 것이다.
  • CAIRO_ANTIALIAS_NONE: 안티 얼레이징이 발생하지 않고, 대신 알파 마스크가 사용될 것이다.
  • CAIRO_ANTIALIAS_GRAY: 안티 얼레이징에 단일 색만 사용하라. 꼭 회색일 필요는 없지만 전경색과 배경색을 기준으로 선택된다.
  • CAIRO_ANTIALIAS_SUBPIXEL: LCD 화면에서 제공되는 하위픽셀 쉐딩(subpixel shading)을 사용하라.


Cairo에 대해 약간의 맛만 보도록 Cairo의 드로잉 컨텍스트를 간략하게 소개해보았다. Cairo에 관한 세부적인 정보는 www.cairographics.org에 실린 API 문서를 참조하도록 한다.


최근 파일

GTK+ 2.10에서는 새로운 API가 소개되어 여러 애플리케이션에 걸쳐서 최근에 연 파일을 추적하도록 해준다. 이번 절에서는 간단한 텍스트 편집 애플리케이션에서 이러한 기능을 구현해볼 것이다. 최근 파일 선택자가 있는 애플리케이션의 모습은 그림 12-4에서 확인할 수 있다. 뒷부분에 실린 연습문제를 통해 개발자의 텍스트 에디터에 최근 파일 지원을 추가해볼 것이다.

그림 12-4. 텍스트 에디터에 사용된 최근 파일 선택자


리스팅 12-7에 실린 코드는 텍스트 편집 애플리케이션을 준비한다. 두 개의 버튼은 GtkFileChooserDialog를 이용해 이미 존재하는 파일을 열고 변경내용을 저장하도록 해준다. 그 다음으로 GtkMenuToolButton이 있는데, 이는 두 개의 함수를 제공한다. 버튼을 클릭하면 GtkRecentChooserDialog가 표시되면서 리스트에서 최근 파일을 선택하도록 해준다. GtkMenuToolButton 위젯 내 메뉴는 가장 최근에 사용한 10개의 파일을 표시하는 GtkRecentChooserMenu 타입이다.


리스팅 12-7. 최근에 연 파일 기억하기 (recentfiles.c)

#include <gtk/gtk.h>

typedef struct
{
    GtkWidget *window;
    GtkWidget *textview;
} Widgets;

static void open_file (GtkButton*, Widgets*);
static void save_file (GtkButton*, Widgets*);
static void open_recent_file (GtkButton*, Widgets*);
static void menu_activated (GtkMenuShell*, Widgets*);

int main (int argc,
        char *argv[])
{
    GtkWidget *vbox, *hbox, *open, *save, *swin, *icon, *menu;
    PangoFontDescription *fd;
    GtkRecentManager *manager;
    Widgets *w;

    gtk_init (&argc, &argv);

    w = g_slice_new (Widgets);
    w->window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
    gtk_window_set_title (GTK_WINDOW (w->window), "Recent Files");
    gtk_container_set_border_width (GTK_CONTAINER (w->window), 5);
    gtk_widget_set_size_request (w->window, 600, 400);

    g_signal_connect (G_OBJECT (w->window), "destroy",
            G_CALLBACK (gtk_main_quit), NULL);

    w->textview = gtk_text_view_new ();
    fd = pango_font_description_from_string ("Monospace 10");
    gtk_widget_modify_font (w->textview, fd);
    pango_font_description_free (fd);

    swin = gtk_scrolled_window_new (NULL, NULL);
    open = gtk_button_new_from_stock (GTK_STOCK_OPEN);
    save = gtk_button_new_from_stock (GTK_STOCK_SAVE);
    icon = gtk_image_new_from_stock (GTK_STOCK_OPEN, GTK_ICON_SIZE_BUTTON);
    w->recent = gtk_menu_tool_button_new (icon, "Recent Files");

    /* Load the default recent chooser menu and create a menu from it. */
    manager = gtk_recent_manager_get_default ();
    menu = gtk_recent_chooser_menu_new_for_manager (manager);
    gtk_menu_tool_button_set_menu (GTK_MENU_TOOL_BUTTON (w->recent), menu);

    gtk_recent_chooser_set_show_not_found (GTK_RECENT_CHOOSER (menu), FALSE);
    gtk_recent_chooser_set_local_only (GTK_RECENT_CHOOSER (menu), TRUE);
    gtk_recent_chooser_set_limit (GTK_RECENT_CHOOSER (menu), 10);
    gtk_recent_chooser_set_sort_type (GTK_RECENT_CHOOSER (menu),
            GTK_RECENT_SORT_MRU);

    g_signal_connect (G_OBJECT (menu), "selection-done",
            G_CALLBACK (menu_activated), (gpointer) w);

    /* ... Connect other signals and populate the window ... */

    gtk_container_add (GTK_CONTAINER (w->window), vbox);
    gtk_widget_show_all (w->window);

    gtk_main ();
    return 0;
}

/* Save the changes that the user made to the file to disk. */
static void
save_file (GtkButton *save,
        Widgets *w)
{
    const gchar *filename;
    gchar *content;
    GtkTextBuffer *buffer;
    GtkTextIter start, end;

    filename = gtk_window_get_title (GTK_WINDOW
 (w->window));
    buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (w->textview));
    gtk_text_buffer_get_bounds (buffer, &start, &end);
    content = gtk_text_buffer_get_text (buffer, &start, &end, FALSE);

    if (!g_file_set_contents (filename, content, -1, NULL))
        g_warning ("The file '%s' could not be written!", filename);
    g_free (content);
}


GtkRecentManager라고 불리는 주요(central) 클래스는 최근 파일 정보를 처리한다. 처음부터 자신만의 파일을 생성할 수도 있지만 최근 파일을 여러 애플리케이션에 걸쳐 공유하고 싶다면 gtk_recent_manager_get_default()를 이용해 기본값을 검색할 수 있다. 이는 GEdit, GNOME의 최근 문서 메뉴, GtkRecentManager API를 활용하는 다른 애플리케이션들과 최근 파일을 공유하도록 해준다.


다음으로 기본 GtkRecentManager로부터 새로운 GtkRecentChooserMenu 위젯을 생성한다. 이 메뉴는 최근 파일을 표시하고, gtk_recent_chooser_menu_new_for_manager()를 이용해 생성된 메뉴 항목에 선택적으로 번호를 매길 것이다. 파일에 기본적으로 번호가 매겨지지는 않지만 이 프로퍼티는 show-numbers를 TRUE로 설정하거나 gtk_recent_chooser_menu_set_show_numbers()를 호출하여 변경이 가능하다.


GtkRecentChooserMenu는 위젯과 상호작용에 필요하게 될 기능을 제공하는 GtkRecentChooser 인터페이스를 구현한다. 리스팅 12-7에서는 메뉴를 맞춤설정하기 위해 다수의 GtkRecentChooser 프로퍼티가 사용되었다. 이들은 GtkRecentChooser 인터페이스를 구현하는 또 다른 위젯 두 개에도 적용되는데, 바로 GtkRecentChooserDialog와 GtkRecentChooserWidget이다.


리스트에 최근 파일이 추가되면 리스트에서 제거하는 것도 가능하다. 이런 경우 리스트에 파일이 표시되는 것은 원치 않을 것이다. 더 이상 존재하지 않는 최근 파일은 gtk_recent_chooser_set_show_not_found()를 이용해 숨길 수 있다. 이 프로퍼티는 로컬 머신에 위치한 파일에만 작용할 것이다.


Gtkd tip.png 찾을 수 없는 파일을 사용자에게 표시하길 원할 수도 있다. 사용자가 존재하지 않는 파일을 선택하면 개발자는 사용자에게 문제를 알린 후 리스트에서 해당 파일을 쉽게 제거할 수 있다.


기본적으로는 로컬 파일만 사용자에게 표시되는데, 즉 file:// 가 앞에 붙은 인터넷 식별자(URI)를 갖게 될 것이란 뜻이다. URI는 접두사를 기반으로 파일 위치나 인터넷 주소 등을 참조하는 데에 사용된다. file:// 접두사만 사용할 경우 로컬 머신에 위치함을 보장할 것이다. 이 프로퍼티를 FALSE로 설정하면 원격 위치에 존재하는 최근 파일을 표시할 수 있다. 원격 파일은 더 이상 존재하지 않을 경우 필터링되지 않음을 명심한다!


리스트에 최근 파일의 수가 많은 경우 메뉴에 모두 열거하는 일은 원치 않을 것이다. 메뉴에 수백 개의 항목이 포함되어 있다면 얼마나 커질지 생각해보라! 따라서 gtk_recent_chooser_set_limit()를 이용하면 메뉴에 표시될 최근 항목의 최대 수를 설정할 수 있다.

void gtk_recent_chooser_set_limit (GtkRecentChooser *chooser,
        gint limit);


요소의 개수를 제한할 때 어떤 파일을 표시할 것인지는 gtk_recent_chooser_set_sort_type()을 이용해 정의한 정렬 타입에 따라 좌우된다. 기본적으로는 GTK_RECENT_SORT_NONE으로 설정된다. GtkRecentSortType 열거에서 이용 가능한 값은 다음과 같다.

  • GTK_RECENT_SORT_NONE: 최근 파일의 리스트가 전혀 정렬되지 않고, 요소가 표시되는 순서대로 리턴될 것이다. 어떤 파일이 표시될 것인지 예측할 수 없으므로 표시할 요소의 개수를 제한한다면 이 값을 사용해선 안 된다.
  • GTK_RECENT_SORT_MRU: 리스트에 가장 최근에 추가한 파일을 먼저 정렬한다. 가장 최근의 파일을 리스트 처음에 위치시키므로 대부분 이 값을 사용할 것이다.
  • GTK_RECENT_SORT_LRU: 리스트에 가장 늦게 추가한 파일을 먼저 정렬한다.
  • GTK_RECENT_SORT_CUSTOM: 최근 파일의 정렬에 커스텀 정렬 함수를 이용한다. 이 값을 이용하려면 사용할 정렬 함수를 정의하도록 gtk_recent_manager_set_sort_func()를 이용해야 한다.


이 예제에서 마지막 부분은 명시된 이름으로 파일을 저장하는 것이다. 텍스트 에디터에서 파일이 열리면 창 제목이 파일명으로 설정된다. 이 파일명은 파일을 저장 시에 사용된다. 이에 따라 이렇게 간단한 텍스트 에디터는 새로운 파일을 생성할 때에는 사용할 수 없으므로 주의하길 바란다!


최근 선택자 메뉴

지금까지 GtkRecentChooserMenu 위젯에 대해 학습하였다. 리스팅 12-8은 리스팅 12-7에서 연결했던 selection-done 콜백 함수를 구현한다. 이 함수는 선택된 URI을 검색하고, 파일이 존재할 경우 파일을 연다.


리스팅 12-8. GtkRecentChooserMenu 이용하기

/* A menu item was activated. So, retrieve the file URI and open it. */
static void
menu_activated (GtkMenuShell *menu,
        Widgets *w)
{
    GtkTextBuffer *buffer;
    gchar *filename, *content, *fn;
    gsize length;

    filename = gtk_recent_chooser_get_current_uri (GTK_RECENT_CHOOSER (menu));

    if (filename != NULL)
    {
        /* Remove the "file://" prefix from the beginning of the URI if it exists. */
        fn = g_filename_from_uri (filename, NULL, NULL);

        if (g_file_get_contents (fn, &content, &length, NULL))
        {
            gtk_window_set_title (GTK_WINDOW (w->window), fn);
            buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (w->textview));
            gtk_text_buffer_set_text (buffer, content, -1);
            g_free (content);
        }
        else
            g_warning ("The file '%s' could not be read!", filename);

        g_free (filename);
        g_free (fn);
    }
}


하나의 항목만 선택 가능하므로 현재 선택된 최근 파일을 검색할 때는 gtk_recent_chooser_get_current_uri()를 이용할 수 있다. 메뉴는 로컬 파일만 표시하도록 제한하였기 때문에 URI에서 file:// 접두사를 제거해야 한다. 원격 파일의 표시도 허용한다면 URI에서 다른 접두사, 즉 http://와 같은 접두사는 제거해야 할 것이다. URI 접두사를 제거하려면 g_filename_from_uri()를 이용하면 된다.

gchar* g_filename_from_uri (const gchar *uri,
        gchar **hostname,
        GError **error);


접두사가 제거되고 나면 GLib는 파일 열기를 시도한다. 파일이 성공적으로 열리면 창 제목이 파일명으로 설정되고 파일이 열린다. 파일 열기가 실패하면 파일을 열 수 없다는 경고가 사용자에게 표시된다.


최근 파일 추가하기

Open 버튼을 누르면 사용자가 GtkFileChooserDialog로부터 열 파일을 선택할 수 있기를 바랄 것이다. 파일이 열리면 리스팅 12-9에서 볼 수 있듯이 기본 GtkRecentManager로 추가될 것이다.


리스팅 12-9. 파일을 열고 최근 파일 리스트로 추가하기

/* Open a file selected by the user and add it as a new recent file. */
static void
open_file (GtkButton *open,
        Widgets *w)
{
    GtkWidget *dialog;
    GtkRecentManager *manager;
    GtkRecentData *data;
    GtkTextBuffer *buffer;
    gchar *filename, *content, *uri;
    gsize length;

    static gchar *groups[2] = {
        "testapp",
        NULL
    };

    dialog = gtk_file_chooser_dialog_new ("Open File", GTK_WINDOW (w->window),
            GTK_FILE_CHOOSER_ACTION_OPEN,
            GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
            GTK_STOCK_OPEN, GTK_RESPONSE_OK,
            NULL);

    if (gtk_dialog_run (GTK_DIALOG (dialog)) == GTK_RESPONSE_OK)
    {
        filename = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (dialog));

        if (g_file_get_contents (filename, &content, &length, NULL))
        {
            /* Create a new recently used resource. */
            data = g_slice_new (GtkRecentData);
            data->display_name = NULL;
            data->description = NULL;
            data->mime_type = "text/plain";
            data->app_name = (gchar*) g_get_application_name ();
            data->app_exec = g_strjoin (" ", g_get_prgname (), "%u", NULL);
            data->groups = groups;
            data->is_private = FALSE;
            uri = g_filename_to_uri (filename, NULL, NULL);

            /* Add the recently used resource to the default recent manager. */
            manager = gtk_recent_manager_get_default ();
            gtk_recent_manager_add_full (manager, uri, data);

            /* Load the file and set the filename as the title of the window. */
            gtk_window_set_title (GTK_WINDOW (w->window), filename);
            buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (w->textview));
            gtk_text_buffer_set_text (buffer, content, -1);

            g_free (content);
            g_free (uri);
            g_free (data->app_exec);
            g_slice_free (GtkRecentData, data);
        }
        else
            g_warning ("The file '%s' could not be read!", filename);

            g_free (filename);
    }

    gtk_widget_destroy (dialog);
}


파일이 성공적으로 열리면 gtk_recent_manager_add_full()을 이용해 파일을 기본 GtkRecentManager에 새로운 최근 항목으로 추가한다. 이 함수를 이용하려면 두 가지 항목이 필요하다. 첫째는 URI가 필요한데, 이는 파일이 로컬 파일임을 나타내도록 file:// 뒤에 파일명을 붙여서 생성된다. 파일명은 g_filename_to_uri()를 이용해 빌드된다.

gchar* g_filename_to_uri (const gchar *filename,
        const gchar *hostname,
        GError **error);


두 번째로 GtkRecentData 구조체의 인스턴스가 필요하다. 이 구조체의 내용은 아래 코드 조각에 표시하였는데, 7개의 매개변수가 포함되어 있다. display_name은 파일명 대신 표시해야 할 짧은 이름에 해당하며, description은 파일의 간략한 설명에 해당한다. 두 값 모두 안전하게 NULL로 설정 가능하다.

typedef struct
{
    gchar *display_name;
    gchar *description;
    gchar *mime_type;
    gchar *app_name;
    gchar *app_exec;
    gchar **groups;
    gboolean is_private;
} GtkRecentData;


다음은, 파일에 대한 MIME 타입, 애플리케이션명, 파일을 여는 데 사용한 명령행을 명시해야 한다. 애플리케이션의 이름은 g_get_application_name()을 호출하여 검색 가능하다. 이후 g_get_prgname()을 이용해 프로그램명을 얻을 수 있다. %f와 %u 문자는 각각 리소스에 대한 파일 경로와 URI를 얻는 데에 사용된다.


다음으로 groups는 리소스가 속한 그룹을 지정하는 문자열 리스트다. 개발자는 이를 이용해 특정 그룹에 속하지 않은 파일을 필터링할 수 있다. 가령 testapp 그룹에 대한 필터가 GtkRecentChooser로 추가되면 이 애플리케이션에 의해 추가된 최근 파일만 표시될 것이다.


마지막 member인 is_private은 리소스를 등록하지 않은 애플리케이션에서도 리소스를 이용할 수 있는지 여부를 명시한다. 이를 TRUE로 설정하면 GtkRecentManager를 이용하는 다른 애플리케이션들은 해당하는 최근 파일을 표시하지 못하도록 방지할 수 있다.


GtkRecentData 인스턴스를 생성했다면 최근 파일 URI와 함께 새로운 리소스로서 추가할 수 있는데, 이는 gtk_recent_manager_add_item()을 통해 이루어진다. gtk_recent_manager_add_item()을 이용해 새로운 최근 항목을 추가할 수도 있는데, 이를 호출하면 알아서 GtkRecentData 객체가 생성될 것이다.


최근 항목을 제거하려면 gtk_recent_manager_remove_item()을 호출하라. 명시된 URI로 된 파일이 성공적으로 제거되면 이 함수는 TRUE를 리턴할 것이다. 만일 파일 제거가 실패하면 GtkRecentManagerError 도메인에서 오류가 설정될 것이다. gtk_recent_manager_purge_items()를 이용해 리스트로부터 최근 파일을 모두 제거할 수도 있다.

gboolean gtk_recent_manager_remove_item (GtkRecentManager *manager,
        const gchar *uri,
        GError **error);


Gtkd caution.png 기본 GtkRecentManager에서 모든 항목을 제거(purging)하는 것은 피하도록 한다! 이는 모든 애플리케이션에서 등록된 최근 항목을 제거하는데, 자신의 애플리케이션에서는 보통 다른 애플리케이션의 최근 리소스를 변경해서는 안 되므로 사용자가 이를 필요로 하는 경우는 드물기 때문이다.


최근 선택자 대화상자

GTK+는 최근 파일을 편의(convenient) 대화상자에 표시하는 GtkRecentChooserDialog라는 위젯도 제공한다. 이 위젯은 GtkRecentChooser 인터페이스를 구현하기 때문에 GtkRecentChooserMenu와 그 기능이 매우 비슷하다. 리스팅 12-10은 사용자가 열어야 할 최근 파일을 선택하기 위해 이 위젯을 이용하는 방법을 보여준다.


리스팅 12-10. GtkRecentChooserDialog 이용하기

/* Allow the user to choose a recent file from the list in the dialog. */
static void
open_recent_file (GtkButton *recent,
        Widgets *w)
{
    GtkWidget *dialog;
    GtkRecentManager *manager;

    GtkTextBuffer *buffer;
    GtkRecentFilter *filter;
    gchar *filename, *content, *fn;
    gsize length;

    manager = gtk_recent_manager_get_default ();
    dialog = gtk_recent_chooser_dialog_new_for_manager ("Open Recent File",
            GTK_WINDOW (w->window), manager,
            GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
            GTK_STOCK_OPEN, GTK_RESPONSE_OK, NULL);

    /* Add a filter that will display all of the files in the dialog. */
    filter = gtk_recent_filter_new ();
    gtk_recent_filter_set_name (filter, "All Files");
    gtk_recent_filter_add_pattern (filter, "*");
    gtk_recent_chooser_add_filter (GTK_RECENT_CHOOSER (dialog), filter);

    /* Add another filter that will only display plain text files. */
    filter = gtk_recent_filter_new ();
    gtk_recent_filter_set_name (filter, "Plain Text");
    gtk_recent_filter_add_mime_type (filter, "text/plain");
    gtk_recent_chooser_add_filter (GTK_RECENT_CHOOSER (dialog), filter);

    gtk_recent_chooser_set_show_not_found (GTK_RECENT_CHOOSER (dialog), FALSE);
    gtk_recent_chooser_set_local_only (GTK_RECENT_CHOOSER (dialog), TRUE);
    gtk_recent_chooser_set_limit (GTK_RECENT_CHOOSER (dialog), 10);
    gtk_recent_chooser_set_sort_type (GTK_RECENT_CHOOSER (dialog),
            GTK_RECENT_SORT_MRU);

    if (gtk_dialog_run (GTK_DIALOG (dialog)) == GTK_RESPONSE_OK)
    {
        filename = gtk_recent_chooser_get_current_uri (GTK_RECENT_CHOOSER (dialog));

        if (filename != NULL)
        {
            /* Remove the "file://" prefix from the beginning of the URI if it exists. */
            fn = g_filename_from_uri (filename, NULL, NULL);

            if (g_file_get_contents (fn, &content, &length, NULL))
            {
                gtk_window_set_title (GTK_WINDOW (w->window), fn);
                buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (w->textview));
                gtk_text_buffer_set_text (buffer, content, -1);
                g_free (content);
            }
            else
                g_warning ("The file '%s' could not be read!", filename);

            g_free (filename);
            g_free (fn);
        }
    }

    gtk_widget_destroy (dialog);

}


새로운 GtkRecentChooserDialog 위젯은 gtk_recent_chooser_dialog_new_for_manager()를 이용해 대화상자와 비슷한 방식으로 생성된다. 이 함수는 대화상자 제목, 부모 창, 표시할 GtkRecentManager 위젯, 버튼과 응답 식별자 쌍을 수락한다.


리스팅 12-10에서는 최근 파일 필터를 소개한다. 새로운 GtkRecentFilter 객체는 gtk_recent_chooser_new()를 이용해 생성된다. 설치된 패턴을 따르는 최근 파일만 표시하도록 필터를 이용하였다.

gtk_recent_filter_set_name (filter, "All Files");
gtk_recent_filter_add_pattern (filter, "*");
gtk_recent_chooser_add_filter (GTK_RECENT_CHOOSER (dialog), filter);


다음 단계는 필터의 이름을 설정하는 일이다. 이 이름은 사용자가 사용할 필터를 선택하는 콤보 박스에 표시될 것이다. 필터를 생성하는 방법에는 여러 가지가 있는데, 일치하는 패턴으로 필터를 찾는 gtk_recent_filter_add_pattern()도 그 중 하나다. 별표 문자를 와일드카드로 이용할 수도 있다. MIME 타입, 이미지 타입, 애플리케이션명, 그룹명, 일자 수(ages in days)를 매칭하기 위한 함수들도 존재한다. 다음으로 gtk_recent_chooser_add_filter()를 이용해 GtkRecentFilter를 최근 선택자로 추가한다.


GtkRecentChooserDialog 위젯의 경우 gtk_recent_chooser_set_select_multiple()을 이용해 다중 파일을 선택하는 것이 가능하다. 사용자가 다중 파일을 선택할 경우, 선택된 파일을 모두 검색하기 위해서는 gtk_recent_chooser_get_uris()를 이용해야 할 것이다.

gchar** gtk_recent_chooser_get_uris (GtkRecentChooser *chooser,
        gsize *length);


이 함수는 NULL로 끝나는 문자열 리스트에 요소의 개수를 리턴하기도 한다. 리스트 사용이 끝나면 g_strfreev()를 이용해 해제해야 한다.


자동 완성

제 4장에서 GtkEntry 위젯에 대해 학습한 바 있지만 GTK+는 GtkEntryCompletion 객체를 제공하기도 한다. GtkEntryCompletion은 GObject에서 파생되며, GtkEntry 위젯에서 사용자에게 자동 완성 기능을 제공할 때 사용 가능하다. 그림 12-5는 사용자에게 다중 선택 기능을 제공하는 GtkEntry의 예를 보여준다. 사용자는 선택을 무시하고 임의의 문자열을 입력할 수도 있음을 주목한다.

그림 12-5. GtkEntryCompletion 자동 완성


리스팅 12-11은 GTK+ 위젯의 이름을 입력하도록 요청하는 GtkEntry 위젯을 구현한다. 입력된 텍스트와 동일한 접두사를 가진 GtkEntryCompletion 위젯 내 문자열은 모두 선택(choice)으로 표시된다. 이 예제는 자동 완성을 준비시키고 실행하는 것이 얼마나 쉬운지를 보여준다.


리스팅 12-11. 자동 완성 (entrycompletion.c)

#include <gtk/gtk.h>

#define NUM_ELEMENTS 4

static gchar *widgets[] = { "GtkDialog", "GtkWindow", "GtkContainer", "GtkWidget" };

int main (int argc,
        char *argv[])

{
    GtkWidget *window, *vbox, *label, *entry;
    GtkEntryCompletion *completion;
    GtkListStore *store;
    GtkTreeIter iter;
    unsigned int i;

    gtk_init (&argc, &argv);

    window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
    gtk_window_set_title (GTK_WINDOW (window), "Automatic Completion");
    gtk_container_set_border_width (GTK_CONTAINER (window), 10);

    label = gtk_label_new ("Enter a widget in the following GtkEntry:");
    entry = gtk_entry_new ();

    /* Create a GtkListStore that will hold autocompletion possibilities. */
    store = gtk_list_store_new (1, G_TYPE_STRING);
    for (i = 0; i < NUM_ELEMENTS; i++)
    {
        gtk_list_store_append (store, &iter);
        gtk_list_store_set (store, &iter, 0, widgets[i], -1);
    }

    completion = gtk_entry_completion_new ();
    gtk_entry_set_completion (GTK_ENTRY (entry), completion);
    gtk_entry_completion_set_model (completion, GTK_TREE_MODEL (store));
    gtk_entry_completion_set_text_column (completion, 0);

    vbox = gtk_vbox_new (FALSE, 5);
    gtk_box_pack_start (GTK_BOX (vbox), label, FALSE, FALSE, 0);
    gtk_box_pack_start (GTK_BOX (vbox), entry, FALSE, FALSE, 0);

    gtk_container_add (GTK_CONTAINER (window), vbox);
    gtk_widget_show_all (window);

    g_object_unref (completion);
    g_object_unref (store);

    gtk_main ();
    return 0;
}


GtkEntryCompletion을 구현하기 위해서는 먼저 선택을 표시하는 새로운 GtkListStore를 생성해야 한다. 이 예제에 실린 모델은 하나의 텍스트 열만 포함하지만 그 하나의 열이 G_TYPE_STRING에 해당하는 한 좀 더 복잡한 GtkListStore를 제공하는 것도 허용된다.


새로운 GtkEntryCompletion 객체는 gtk_entry_completion_new()를 이용해 생성된다. 이렇게 생긴 객체는 후에 gtk_entry_set_completion()을 이용해 기존 GtkEntry 위젯으로 적용할 수도 있다. GTK+는 매치된 내용을 표시하고 선택을 적용하는 일을 기본적으로 알아서 처리할 것이다.


다음은 gtk_entry_completion_set_model()을 이용해 트리 모델을 GtkEntryCompletion 객체로 적용한다. 객체로 적용된 모델이 이미 존재한다면 대체될 것이다. gtk_entry_completion_set_text_column()을 이용하면 어떤 열이 문자열을 포함하는지 지정할 수 있는데, 모델에는 꼭 하나의 열만 있으란 법은 없기 때문이다. 텍스트 열을 설정하지 않으면 텍스트 열이 기본적으로 -1로 설정되어 자동 완성이 작동하지 않을 것이다.


gtk_entry_completion_set_inline_completion()을 이용하면 모든 매치에 공통된 접두사를 원하는 만큼 표시할 수가 있다. 인라인 완성(inline completion)은 대·소문자를 구분하지만 자동 완성에서는 구분되지 않음을 명심해야 한다! 인라인 완성을 이용 시 하나의 매치만 있을 때에는 팝업 메뉴가 표시되지 않도록 하려면 gtk_entry_completion_set_popup_single_match()를 설정해야 할 것이다.


gtk_entry_completion_set_popup_set_width()를 이용하면 강제로 팝업 메뉴의 너비를 GtkEntry 위젯과 동일한 너비로 만든다. 이는 GtkEntryCompletion의 popup-set-width 프로퍼티와 동일하다.


매치가 너무 많다면 gtk_entry_completion_set_minimum_key_length()를 이용해 최소 매치 길이를 설정하길 원할 것이다. 이 함수는 리스트에 요소의 수가 너무 많아서 화면에 렌더링하는 데에 오랜 시간이 소요되는 경우에 유용하겠다.


자신의 이해도 시험하기

이번 장에 실린 연습문제에서는 앞의 여러 장에 실린 연습문제에서 집중적으로 살펴본 텍스트 편집 애플리케이션을 완성할 것이다. 그러기 위해서는 자동 완성, 인쇄, 최근 파일 기능을 애플리케이션으로 통합해야 할 것이다.


연습문제 12-1. 전체 텍스트 에디터 생성하기

이번 연습문제에서는 앞의 몇 장에 걸쳐 생성한 텍스트 에디터를 완성할 것이다. 먼저 애플리케이션에 세 가지 새로운 기능을 추가할 것이다.


첫 번째로 검색 툴바에 지난 검색을 기억하기 위해 구현되어야 하는 자동 완성 기능을 추가하라. 애플리케이션은 애플리케이션 런타임의 현재 인스턴스에 대한 과거의 검색만 기억해야 한다.


다음으로 인쇄 지원을 추가해야 하는데, 인쇄 지원에는 인쇄하기와 인쇄 미리보기 기능이 포함된다. 인쇄 지원은 고수준의 GtkPrintOperation API로 쉽게 구현이 가능하다. 마지막으로 GtkRecentManager API를 이용해 텍스트 에디터로 하여금 마지막으로 로딩된 5개의 파일을 기억하도록 지시하라.


앞에서 살펴본 애플리케이션의 측면은 재작성할 필요가 없으므로 제 10장의 연습문제 해답을 이용하거나 본 서적의 웹 사이트에서 해답을 다운로드하도록 한다.


요약

이번 전에서는 앞의 여러 장에서 다룬 주제에 해당하지 않았던 다수의 위젯들을 학습해보았다. 이러한 위젯과 객체를 요약하자면 다음과 같다.

  • GtkDrawingArea: GdkWindow 객체에 그리도록 해주는 빈 위젯으로, 이 또한 GdkDrawable 객체다.
  • GtkLayout: 고유의 인터페이스에도 위젯을 포함할 수 있도록 해준다는 점을 제외하면 GtkDrawingArea와 동일하다. 오버헤드를 야기할 수 있으므로 그리기 기능만 원할 때는 이 기능을 사용해선 안 된다.
  • GtkCalendar: 선택한 연도에 하나의 월을 표시한다. 이 위젯은 사용자가 일자를 선택하도록 해주며, 개발자는 프로그램적으로 여러 개의 일자를 표시할 수 있다.
  • GtkStatusIcon: 지원되는 플랫폼에서 작업 표시줄에 아이콘을 표시하여 사용자에게 메시지를 제공한다. 팝업 메뉴 또는 툴팁을 제공하고 아이콘이 활성화될 때를 감지할 수도 있다.
  • GtkPrintOperation: 플랫폼 독립적인 고수준의 인쇄 API다. 인쇄 지원을 구현하기 위해 많은 객체들이 제공되지만 대부분의 액션은 GtkPrintOperation으로 처리되어야만 여러 플랫폼에서 기능할 것이다.
  • GtkRecentManager: 최근 파일 리스트를 관리하기 위한 간단한 API다. 이 리스트는 여러 애플리케이션에서 공유가 가능하다. 최근 파일을 표시하도록 메뉴와 대화상자 위젯이 제공된다.
  • GtkEntryCompletion: GtkEntry 위젯에 자동 완성을 지원한다. 선택(choice)은 가능한 매치 값들로 채워진 GtkListStore 객체로 구성된다.


이제 본 서적에서 다루기로 한 모든 주제들을 학습하였다. 다음 장에서는 총 12개의 장에서 다룬 주제들을 이용해 5개의 완전한 애플리케이션을 소개하고자 한다.


Notes