FoundationsofGTKDevelopment:Chapter 05

From 흡혈양파의 번역工房
Jump to navigation Jump to search
제 5 장 대화상자

대화상자

이번 장에서는 대화상자(dialog)라고 불리는 특별한 유형의 창을 소개하고자 한다. 대화상자는 최상위 수준 창을 보완하는 창이다. 대화상자는 GtkWindow의 자식 클래스인 GtkDialog에 의해 제공되며 추가 기능이 확장된다. 즉, 메인 창은 숨겨둔 채로 하나 또는 그 이상의 대화상자에서 자신의 전체 인터페이스를 구현하는 것이 가능하다는 의미다.


대화상자에서는 메시지를 표시하거나 사용자에게 옵션을 선택하도록 요청하는 것을 비롯해 어떤 일이든 할 수 있다. 대화상자의 목적은 몇 가지 일시적인 기능을 제공함으로써 사용자의 경험을 향상시키는 데에 있다.


본 장에서는 먼저 자신만의 커스텀 대화상자를 생성하기 위해 GtkDialog를 사용하는 방법을 익힐 것이다. 다음 절에서는 GTK+가 제공하는 내장된 대화상자를 다수 소개할 것이다. 마지막으로, 여러 페이지로 구성된 대화상자를 생성하게 해주는 GtkAssistant라는 위젯을 학습할 것인데, assistant는 사용자를 여러 단계에 거쳐 도와주도록 되어 있다.


이번 장에서는 다음을 학습할 것이다.

  • GtkDialog 위젯을 이용해 자신만의 커스텀 대화상자를 생성하는 방법
  • GtkMessageDialog 위젯을 이용해 사용자에게 일반적인 정보, 오류 메시지, 경고를 제공하는 방법
  • GtkAboutDialog를 이용해 자신의 애플리케이션에 관한 정보를 제공하는 방법
  • 이용 가능한 파일 선택자 대화상자의 유형
  • 글꼴과 색상 선택 대화상자를 이용해 정보를 수집하는 방법
  • GtkAssistant 위젯을 이용해 여러 페이지로 구성된 대화상자를 생성하는 방법


자신만의 커스텀 대화상자 생성하기

대화상자는 특별한 유형의 GtkWindow로서 최상위 수준의 창을 보완하는 데에 사용된다. 사용자에게 메시지를 제공하거나, 사용자로부터 정보를 검색하거나, 다른 임의의(transient) 액션 타입을 제공하는 데에 사용할 수 있겠다.


대화상자 위젯은 수평 구분자에 의해 절반으로 나뉜다. 윗부분은 대화상자의 사용자 인터페이스에서 주요 부분을 위치시키는 곳이다. 아랫부분은 액션 영역(action area)이라 불리며, 버튼의 컬렉션을 보유한다. 각 버튼을 클릭하면 프로그래머에게 어떤 버튼이 클릭되었는지 알려주는 유일한 응답 식별자를 방출할 것이다.


대부분은 대화상자를 하나의 창으로서 취급할 수 있는데, GtkWindow 클래스로부터 파생되기 때문이다. 하지만 창이 여러 개인 경우 대화상자가 최상위 수준 창을 보완해야 한다면 대화상자와 최상위 수준 창 사이에 부모-자식 관계가 구축되어야 한다.

typedef struct
{
    GtkWidget *vbox;
    GtkWidget *action_area;
} GtkDialog;


GtkDialog는 액션 영역이라 불리는 수평 버튼 박스와 수직 박스를 포함하는 두 개의 public member를 제공한다. 액션 영역은 대화상자의 하단을 따라 모든 버튼을 포함한다. GtkHButtonBox를 이용해 액션 영역에 수동으로 버튼을 추가할 수 있지만 액션 영역 위젯을 추가하기 위해서는 보통 GtkDialog가 제공하는 함수를 사용해야 한다.


Gtkd note.png 모두 동일한 위젯이 있는 GtkWindow를 생성하고 gtk_window_set_transient_for()를 이용해 창의 관계를 구축함으로써 GtkWindow가 제공하는 다른 함수들 외에 GtkDialog의 기능을 수동으로 구현할 수 있다. GtkDialog는 표준 방법을 제공하는 편의 위젯에 불과하다.


액션 영역과 구분자 모두 대화상자의 수직 박스 끝에 패킹된다. GtkvBox(vbox)는 대화상자의 모든 내용을 보유하는 데에 사용된다. 액션 영역은 끝에서 패킹되므로 GtkDialog로 위젯을 추가하려면 gtk_box_pack_start() 또는 gtk_box_pack_start_defaults()를 이용해야 한다.

gtk_box_pack_start_defaults (GTK_BOX (dialog->vbox), child);


상자의 시작에 위젯을 패킹함으로써 액션 영역과 구분자는 항상 대화상자의 하단에 남아 있을 것이다.


메시지 대화상자 생성하기

대화상자의 내용이 얼마나 복잡하든 대화상자마다 똑같이 기본적인 개념을 적용할 수 있다는 점이 GtkDialog의 장점으로 들 수 있다. 이를 설명하기 위해 사용자에게 메시지를 제공하는 매우 간단한 대화상자를 생성해보겠다. 그림 5-1에 이러한 대화상자의 스크린샷을 실었다.

그림 5-1. 프로그램적으로 생성된 메시지 대화상자


리스팅 5-1은 버튼에 의해 clicked 시그널이 방출되면 사용자에게 알려주는 간단한 대화상자를 생성한다. 해당 기능은 GtkMessageDialog 위젯에 의해 제공되는데, 머지않아 다룰 위젯이다.


리스팅 5-1. 자신만의 첫 커스텀 대화상자 (dialogs.c)

#include <gtk/gtk.h>

static void button_clicked (GtkButton*, GtkWindow*);

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

    gtk_init (&argc, &argv);

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

    button = gtk_button_new_with_mnemonic ("_Click Me");

    g_signal_connect (G_OBJECT (button), "clicked",
        G_CALLBACK (button_clicked),
        (gpointer) window);

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

    gtk_main ();
    return 0;
}

/* Create a new GtkDialog that will tell the user that the button was clicked. */
static void
button_clicked (GtkButton *button,
        GtkWindow *parent)
{
    GtkWidget *dialog, *label, *image, *hbox;

    /* Create a new dialog with one OK button. */
    dialog = gtk_dialog_new_with_buttons ("Information", parent,
        GTK_DIALOG_MODAL,
        GTK_STOCK_OK, GTK_RESPONSE_OK,
        NULL);

    gtk_dialog_set_has_separator (GTK_DIALOG (dialog), FALSE);

    label = gtk_label_new ("The button was clicked!");
    image = gtk_image_new_from_stock (GTK_STOCK_DIALOG_INFO,
        GTK_ICON_SIZE_DIALOG);

    hbox = gtk_hbox_new (FALSE, 5);
    gtk_container_set_border_width (GTK_CONTAINER (hbox), 10);
    gtk_box_pack_start_defaults (GTK_BOX (hbox), image);
    gtk_box_pack_start_defaults (GTK_BOX (hbox), label);

    /* Pack the dialog content into the dialog's GtkVBox. */
    gtk_box_pack_start_defaults (GTK_BOX (GTK_DIALOG (dialog)->vbox), hbox);
    gtk_widget_show_all (dialog);

    /* Create the dialog as modal and destroy it when a button is clicked. */
    gtk_dialog_run (GTK_DIALOG (dialog));
    gtk_widget_destroy (dialog);
}


대화상자 생성하기

메인 창에 있는 버튼을 클릭하면 가장 먼저 해야 할 일은 gtk_dialog_new_with_buttons()를 이용해 GtkDialog 위젯을 생성하는 것이다. 해당 함수에서 첫 두 개의 매개변수는 대화상자의 제목과 부모 창에 대한 포인터를 명시한다.

GtkWidget* gtk_dialog_new_with_buttons (const gchar *title,
        GtkWindow *parent,
        GtkDialogFlags flags,
        const gchar *first_button_text,
        ...);


대화상자는 부모 창의 임의 창으로 설정되어, 윈도 관리자가 메인 창 위에 대화상자를 중앙으로 정렬시켜(center) 필요 시 젤 위에 위에 표시하도록(keep in on top) 해준다. gtk_window_set_transient_for()를 호출하면 임시 창에서도 가능하다. 대화상자가 부모 창을 갖거나 인식하는 것을 원치 않는다면 NULL을 제공하면 된다.


다음으로 당신은 하나 또는 그 이상의 대화상자 플래그를 명시할 수 있다. 해당 매개변수에 대한 옵션은 GtkDialogFlags 목록에서 주어진다. 이용 가능한 값으로는 아래와 같이 세 가지가 있다.

  • GTK_DIALOG_MODAL: 대화상자가 닫힐 때까지 강제로 부모 창의 위에 포커스를 받은 채 유지되도록 한다. 사용자와 부모 창의 상호작용을 막을 것이다.
  • GTK_DIALOG_DESTROY_WITH_PARENT: 부모 창이 소멸되면 대화상자도 소멸되지만 강제로 대화상자에 포커스를 두지 않는다. 이는 gtk_dialog_run()을 호출하지 않는 한 비모드형(nonmodal) 대화상자를 생성할 것이다.
  • GTK_DIALOG_NO_SEPARATOR: 설정 시 구분자가 액션 영역과 대화상자 내용 사이에 위치하지 않을 것이다.


리스팅 5-1에서 GTK_DIALOG_MODAL을 명시하니 모드형(modal) 대화상자가 생성되었다. 제목이나 부모 창을 명시할 필요는 없는데, 값이 NULL로 설정될 수도 있기 때문이다. 하지만 제목은 항상 설정해야 하는데, 그래야만 윈도 관리자에 그릴 수 있기 때문이다. 이를 설정하지 않을 경우 사용자는 원하는 창을 선택하는 데에 어려움을 겪을 것이다.


마지막으로 NULL로 끝나는 액션 영역 버튼 및 그들의 응답 식별자 리스트도 명시되어야 한다. 리스팅 5-1에서는 GTK_RESPONSE_OK의 응답이 있는 OK 버튼이 대화상자에 추가되었다.


그렇지 않으면 gtk_dialog_new()를 이용해 빈 대화상자를 생성할 수도 있으나, 이러한 경우 gtk_dialog_add_button() 또는 gtk_dialog_add_buttons()를 이용해 수동으로 버튼을 추가할 필요가 있을 것이다. 대부분의 경우 리스팅 5-1에서와 동일한 방식으로 대화상자를 생성하는 편이 수월하다.


기본적으로 모든 대화상자는 대화상자의 주 내용과 액션 영역 사이에 수평 구분자를 위치시킨다. 하지만 일부 사례에서는 이번 예제에서와 같이 구분자를 숨기는 편이 바람직하다. 이때는 gtk_dialog_set_has_separator()를 이용하면 된다.

void gtk_dialog_set_has_separator (GtkDialog *dialog,
        gboolean has_separator);


자식 위젯이 생성되었다면 대화상자로 추가할 필요가 있겠다. 앞서 언급하였듯 자식 위젯은 gtk_box_pack_start_defaults() 또는 gtk_box_pack_start()를 호출하여 대화상자로 추가된다. 대화상자는 vbox라고 불리는 public member를 갖는데, 아래와 같이 자식 위젯은 이곳으로 패킹된다.

gtk_box_pack_start_defaults (GTK_BOX (GTK_DIALOG (dialog)->vbox), hbox);
gtk_widget_show_all (dialog);


이 시점에서 대화상자와 그 자식 위젯을 표시할 필요가 있는데, gtk_dialog_run()은 대화상자 자체에 gtk_widget_show()만 호출할 것이기 때문이다. 이를 위해선 대화상자나 그 GtkVBox에서 gtk_widget_show_all()을 호출해야 한다. 위젯을 표시하지 않으면 gtk_dialog_run()이 호출될 때 구분자와 액션 영역만 표시될(visible) 것이다.


응답 식별자

대화상자가 완전히 구성되면 대화상자를 표시하는 한 가지 방법은 gtk_dialog_run()를 호출하는 것이다. 해당 함수는 완료 시 응답 식별자라 불리는 정수를 리턴할 것이다. 이는 대화상자가 소멸되거나 액션 영역 버튼을 클릭할 때까지 대화상자 외부의 것과 상호작용을 하지 못하도록 막을 것이다.

gint gtk_dialog_run (GtkDialog *dialog);


내부적으로 gtk_dialog_run()은 대화상자에 대한 새로운 메인 루프를 생성하여 응답 식별자가 방출되거나 사용자가 대화상자를 닫을 때까지 개발자가 그 부모 창과 상호작용을 하지 못하도록 막을 것이다. 개발자가 설정한 대화상자 플래그가 무엇이든 상관없이 해당 함수를 호출하면 대화상자는 항상 모드형이 될 것인데 그 함수가 gtk_window_set_modal()을 호출하기 때문이다.


윈도 관리자가 제공하는 방식을 이용해 대화상자가 수동으로 소멸될 경우 GTK_RESPONSE_NONE이 리턴된다. 그 외의 경우 gtk_dialog_run()은 클릭된 버튼을 참조하는 응답 식별자를 리턴한다. GtkResponseType 목록에서 이용 가능한 응답 식별자의 전체 리시트를 표 5-1에 표시하였다. 향후 GTK+의 새로운 버전에서 변경될 수 있으므로 무작위 정수 값 대신 항상 식별자의 전처리기 지시어를 사용하도록 한다.

식별자 설명
GTK_RESPONSE_NONE -1 윈도 관리자가 대화상자를 소멸시키거나 gtk_widget_destroy()를 이용해 프로그램적으로 소멸된다. 응답 위젯이 응답 식별자 집합을 갖고 있지 않은 경우에도 리턴된다.
GTK_RESPONSE_REJECT -2 해당 식별자는 내장된 대화상자에 포함된 버튼과 연관되지 않지만 본인의 재량껏 사용해도 좋다.
GTK_RESPONSE_ACCEPT -3 해당 식별자는 내장된 대화상자에 포함된 버튼과 연관되지 않지만 본인의 재량껏 사용해도 좋다.
GTK_RESPONSE_DELETE_EVENT -4 각 대화상자는 자동으로 delete-event 시그널로 연결된다. gtk_dialog_run()이 실행되는 동안 해당 식별자가 리턴될 것이며, delete-event는 언제나처럼 창을 소멸시키지 못할 것이다.
GTK_RESPONSE_OK -5 내장된 대화상자에서 GTK_STOCK_OK 버튼이 클릭되었다. 자신만의 대화상자에서 재량껏 해당 버튼을 사용해도 좋다.
GTK_RESPONSE_CANCEL -6 내장된 대화상자에서 GTK_STOCK_CANCEL 버튼이 클릭되었다.
GTK_RESPONSE_CLOSE -7 내장된 대화상자에서 GTK_STOCK_CLOSE 버튼이 클릭되었다.
GTK_RESPONSE_YES -8 내장된 대화상자에서 GTK_STOCK_YES 버튼이 클릭되었다.
GTK_RESPONSE_NO -9 내장된 대화상자에서 GTK_STOCK_NO 버튼이 클릭되었다.
GTK_RESPONSE_APPLY -10 내장된 대화상자에서 GTK_STOCK_APPLY 버튼이 클릭되었다.
GTK_RESPONSE_HELP -11 내장된 대화상자에서 GTK_STOCK_HELP 버튼이 클릭되었다.
표 5-1. GtkResponseType 목록 값


물론 자신만의 대화상자를 생성하거나 지금부터 몇 페이지에 걸쳐 다루게 될 내장된 대화상자들 중 다수를 사용할 때는 사용할 응답 식별자를 원하는대로 선택해도 좋다. 하지만 OK 버튼에 GTK_RESPONSE_CANCEL 식별자를 적용하거나 그러한 행에 따라 다른 타입을 적용하려는 욕구는 거부해야 한다.


Gtkd note.png 자신만의 응답 식별자를 생성해도 좋지만 모든 내장된 식별자는 음수이므로 양수를 사용해야 한다. 그래야만 향후 GTK+ 버전에서 더 많은 식별자를 추가할 때 충돌을 피할 수 있다.


대화상자가 응답 식별자를 리턴하고 나면 gtk_widget_destroy()를 호출해야 하는데, 그렇지 않을 경우 메모리 누수를 야기할 것이다. GTK+는 대화상자의 모든 자식들이 소멸되도록 확보해야 하지만 그러한 과정을 시작해야 함을 기억해야 한다.


gtk_widget_destroy()를 호출하면 부모 위젯의 모든 자식들이 소멸되고, 그 참조 계수는 떨어질 것이다. 객체의 참조 계수가 0에 도달하면 객체는 finalize되고, 메모리는 해제된다.


GtkImage 위젯

리스팅 5-1에서는 GtkImage라고 불리는 새로운 위젯을 소개한다. 이미지는 다양한 방식으로 로딩이 가능하지만 로딩이 실패할 경우 GTK_STOCK_MISSING_IMAGE 아이콘을 표시할 수 있다는 것이 GtkImage의 장점이다. 이는 GtkWidget에서 파생되므로, GdkPixbuf와 같은 이미지 객체와는 달리 컨테이너 위젯의 자식으로서 추가가 가능하다.


우리 예제에서는 gtk_image_new_from_stock() 이 스톡 항목으로부터 GtkImage 위젯을 생성하였다.

GtkWidget* gtk_image_new_from_stock (const gchar *stock_id,
        GtkIconSize size);


이미지를 로딩할 때는 이미지에 대한 크기도 명시할 필요가 있다. GTK+는 주어진 크기로 된 스톡 아이콘을 자동으로 검색하고, 찾을 수 없는 경우 이미지를 해당 크기로 조정할 것이다. 이용 가능한 크기 매개변수는 GkIconSize 목록에 의해 명시되며, 아래 리스트에서 확인할 수 있다.

  • GTK_ICON_SIZE_INVALID: 크기 명시되지 않음
  • GTK_ICON_SIZE_MENU: 16 ×16 픽셀
  • GTK_ICON_SIZE_SMALL_TOOLBAR: 18 ×18 픽셀
  • GTK_ICON_SIZE_LARGE_TOOLBAR: 24 ×24 픽셀
  • GTK_ICON_SIZE_BUTTON: 24 × 24 픽셀
  • GTK_ICON_SIZE_DND: 32 ×32 픽셀
  • GTK_ICON_SIZE_DIALOG: 48 ×48 픽셀


확인할 수 있듯이 스톡 GtkImage 객체는 주로 버튼이나 메뉴, 대화상자에 나타나는 작은 이미지에 사용되는데, 스톡 이미지는 별개의 표준 크기로 제공되기 때문이다. 리스팅 5-1에서 이미지는 GTK_ICON_SIZE_DIALOG 또는 48x48 픽셀로 설정되었다.


GtkImage에 대한 여러 초기화(initialization) 함수는 API 문서에서 찾아볼 수 있지만 gtk_image_new_from_file()과 gtk_image_new_from_pixbuf()는 특히 본 저서의 뒤에 실린 예제에서 중요한 함수들이다.

GtkWidget *gtk_image_new_from_file (const gchar *filename);


GtkImage는 gtk_image_new_from_file()에 명시된 파일의 이미지 타입을 자동으로 감지할 것이다. 이미지를 로딩할 수 없는 경우 깨진 이미지 아이콘을 표시할 것이다. 따라서 해당 함수가 NULL 객체를 리턴하는 일은 절대 없을 것이다. GtkImage는 이미지 파일 내에서 발생하는 애니메이션도 지원한다.


gtk_image_new_from_pixbuf()를 호출하면 이전에 초기화된 GdkPixbuf로부터 새로운 GtkImage 위젯을 생성한다. 이 함수는 gtk_image_new_from_file()과 달리 먼저 GdkPixbuf를 생성해야 하기 때문에 이미지가 성공적으로 로딩되었는지 쉽게 확인하도록 해준다.

GtkWidget *gtk_image_new_from_pixbuf (GdkPixbuf *pixbuf);


GtkImage는 GdkPixbuf에 대한 고유의 참조를 생성할 것이므로 GtkImage를 이용해 소멸되어야 한다면 객체에 대한 개발자의 참조를 해제(release)할 필요가 있다는 사실을 명심해야 한다.


비모드형 메시지 대화상자

gtk_dialog_run()을 호출하면 개발자의 대화상자는 항상 모드형으로 설정되는데, 이것이 항상 바람직한 것은 아니다. 비모드형 대화상자를 생성하려면 GtkDialog의 response 시그널로 연결할 필요가 있다.


리스팅 5-2에서는 그림 5-1에 소개한 메시지 대화상자를 비모드형 대화상자로서 재구현하였다. 메인 창에 있는 버튼을 연달아 여러 번 클릭해보라. 그러면 같은 대화상자의 다수의 인스턴스를 생성하는 방법 뿐만 아니라 비모드형 대화상자에서 메인 창으로 접근하는 방법도 보여줄 것이다.


리스팅 5-2. 비모드형 메시지 대화상자 (dialogs2.c)

static void
button_clicked (GtkButton *button,
        GtkWindow *parent)
{
    GtkWidget *dialog, *label, *image, *hbox;

    /* Create a nonmodal dialog with one OK button. */
    dialog = gtk_dialog_new_with_buttons ("Information", parent,
        GTK_DIALOG_DESTROY_WITH_PARENT,
        GTK_STOCK_OK, GTK_RESPONSE_OK,
        NULL);

    gtk_dialog_set_has_separator (GTK_DIALOG (dialog), FALSE);

    label = gtk_label_new ("The button was clicked!");
    image = gtk_image_new_from_stock (GTK_STOCK_DIALOG_INFO,
        GTK_ICON_SIZE_DIALOG);

    hbox = gtk_hbox_new (FALSE, 5);
    gtk_container_set_border_width (GTK_CONTAINER (hbox), 10);
    gtk_box_pack_start_defaults (GTK_BOX (hbox), image);
    gtk_box_pack_start_defaults (GTK_BOX (hbox), label);

    gtk_box_pack_start_defaults (GTK_BOX (GTK_DIALOG (dialog)->vbox), hbox);
    gtk_widget_show_all (dialog);

    /* Call gtk_widget_destroy() when the dialog emits the response signal. */
    g_signal_connect
 (G_OBJECT (dialog), "response",
        G_CALLBACK (gtk_widget_destroy), NULL);
}


비모드형 위젯을 생성하는 것은 앞의 예제와 매우 유사한데, gtk_dialog_run()의 호출을 원하지 않는다는 점에서만 차이가 있다. 해당 함수를 호출하면 대화상자 플래그와 상관없이 부모 창의 메인 루프를 막음으로써 모드형 대화상자가 생성된다.


Gtkd tip.png GTK_DIALOG_MODAL 플래그를 설정함으로써 gtk_dialog_run()을 이용할 필요 없이 모드형 대화상자를 생성할 수 있다. 이후 response 시그널로 연결하면 된다. 해당 함수는 모드형 대화상자를 생성하고 하나의 함수 내에서 응답 식별자를 처리하는 편리한 방법을 제공할 뿐이다.


GtkDialog의 response 시그널로 연결함으로써 개발자는 방출되어야 하는 응답 식별자를 기다릴 수 있다. 이러한 방식을 이용하면 응답 식별자가 방출될 때 대화상자가 자동으로 참조해제(unreferenced)되지 않을 것이다. response 콜백 함수는 대화상자, 방출된 응답 식별자, 선택적 데이터 매개변수를 수신하다.


대화상자를 디자인할 때 개발자에게 요구되는 가장 중요한 결정 중 하나가 바로 모드형으로 만들 것인지 비모드형으로 만들 것인지가 된다. 경험으로 보건대, 액션이 완료되어야만 사용자가 애플리케이션으로 작업을 계속할 수 있는 경우 대화상자는 모드형이 되어야 한다. 이와 관련된 예제로 메시지 대화상자, 사용자에게 질문을 하는 대화상자, 파일을 열어야 하는 대화상자를 들 수 있다.


대화상자가 열려 있을 때 사용자가 작업을 계속할 수 없는 이유가 따로 없다면 비모드형 대화상자를 사용해야 한다. 프로그램적으로 방지하지 않는 한 비모드형 대화상자의 여러 인스턴스를 생성하여 하나의 인스턴스만 가져야 하는 대화상자를 모드형으로 생성할 수 있도록 해야 함을 기억해야 한다.


또 다른 대화상자 예제

이제 처음부터 간단한 메시지를 생성해봤으니 조금 더 복잡한 대화상자를 생성해볼 수 있겠다. 리스팅 5-3에서는 사용자에 관한 기본 정보를 GLib의 유틸리티 함수를 이용해 학습하였다. 그림 5-2에 실린 대화상자는 이러한 정보 조각을 편집하도록 해준다.

그림 5-2. 단순한 GtkDialog 위젯


이 정보는 물론 사용자의 시스템 내에서 변경되지 않으며, 새 텍스트는 화면으로 출력된다. 해당 예제는, 대화상자의 복잡성과 상관없이 응답 식별자를 처리하는 방식에 대한 기본 원칙이 여전히 필요하다는 사실을 보여준다.


이를 비모드형 대화상자로도 쉽게 구현할 수 있겠지만 대화상자 자체가 애플리케이션의 최상위 수준 창이기 때문에 별 소용이 없을 것이다.


리스팅 5-3. 대화상자에서 정보 편집하기 (dialogs3.c)

#include <gtk/gtk.h>

int main (int argc,
        char *argv[])
{
    GtkWidget *dialog, *table, *user, *real, *home, *host;
    GtkWidget *lbl1, *lbl2, *lbl3, *lbl4;
    gint result;

    gtk_init (&argc, &argv);

    dialog = gtk_dialog_new_with_buttons ("Edit User Information", NULL
        GTK_DIALOG_MODAL,
        GTK_STOCK_OK, GTK_RESPONSE_OK,
        GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
        NULL);

    gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_OK);

    /* Create four entries that will tell the user what data to enter. */
    lbl1 = gtk_label_new ("User Name:");
    lbl2 = gtk_label_new ("Real Name:");
    lbl3 = gtk_label_new ("Home Dir:");
    lbl4 = gtk_label_new ("Host Name:");

    user = gtk_entry_new ();
    real = gtk_entry_new ();
    home = gtk_entry_new ();
    host = gtk_entry_new ();

    /* Retrieve the user's information for the default values. */
    gtk_entry_set_text (GTK_ENTRY (user), g_get_user_name());
    gtk_entry_set_text (GTK_ENTRY (real), g_get_real_name());
    gtk_entry_set_text (GTK_ENTRY (home), g_get_home_dir());
    gtk_entry_set_text (GTK_ENTRY (host), g_get_host_name());

    table = gtk_table_new (4, 2, FALSE);
    gtk_table_attach_defaults (GTK_TABLE (table), lbl1, 0, 1, 0, 1);
    gtk_table_attach_defaults (GTK_TABLE (table), lbl2, 0, 1, 1, 2);
    gtk_table_attach_defaults (GTK_TABLE (table), lbl3, 0, 1, 2, 3);
    gtk_table_attach_defaults (GTK_TABLE (table), lbl4, 0, 1, 3, 4);

    gtk_table_attach_defaults (GTK_TABLE (table), user, 1, 2, 0, 1);
    gtk_table_attach_defaults (GTK_TABLE (table), real, 1, 2, 1, 2);
    gtk_table_attach_defaults (GTK_TABLE (table), home, 1, 2, 2, 3);
    gtk_table_attach_defaults (GTK_TABLE (table), host, 1, 2, 3, 4);

    gtk_table_set_row_spacings (GTK_TABLE (table), 5);
    gtk_table_set_col_spacings (GTK_TABLE (table), 5);
    gtk_container_set_border_width (GTK_CONTAINER (table), 5);

    gtk_box_pack_start_defaults (GTK_BOX (GTK_DIALOG (dialog)->vbox), table);
    gtk_widget_show_all (dialog);

    /* Run the dialog and output the data if the user clicks the OK button. */
    result = gtk_dialog_run (GTK_DIALOG (dialog));
    if (result == GTK_RESPONSE_OK)
    {
        g_print ("User Name: %s\n", gtk_entry_get_text (GTK_ENTRY (user)));
        g_print ("Real Name: %s\n", gtk_entry_get_text (GTK_ENTRY (real)));
        g_print ("Home Folder: %s\n", gtk_entry_get_text (GTK_ENTRY (home)));
        g_print ("Host Name: %s\n", gtk_entry_get_text (GTK_ENTRY (host)));
    }

    gtk_widget_destroy (dialog);
    return 0;
}


어떠한 모드형 대화상자든 응답 식별자를 이용하여 클릭된 버튼을 기반으로 올바른 응답을 도출하는 방법이야말로 적절한 처리 방법이다. 의도적으로 감지해야 하는 응답은 하나였기 때문에 리스팅 5-3에선 조건적 if 문이 사용되었다.


하지만 다수의 응답 식별자를 처리할 필요가 있다고 가정해보자. 이런 경우 switch() 문이 더 나은 해답이 될 것인데, 아래 코드에서 볼 수 있듯이 단일 변수가 다중 선택에 비교하도록 생성되었기 때문이다.

result = gtk_dialog_run (GTK_DIALOG (dialog));
switch (result)
{
    case (GTK_RESPONSE_OK):
        /* ... Handle the response ... */
        break;
    case (GTK_RESPONSE_APPLY):
        /* ... Handle the response ... */
        break;
    default:
        break;
}

gtk_widget_destroy (dialog);


각 사례에서 대화상자가 소멸되어야 한다면 switch() 문에서 break할 수 있다. switch() 문을 이용해 하나의 사례만 확인해도 된다면 이는 다시 기본 사례가 되어 어떤 응답 식별자가 방출되든 대화상자를 소멸하도록 설정될 것이다.


내장된 대화상자

GTK+에는 이미 내장된 대화상자의 타입이 많다. 이용 가능한 대화상자를 모두 이번 장에서 다루지는 않겠지만 내장된 대화상자를 사용하는 데에 필요한 개념은 확실히 이해할 것이다. 이번 절에서는 GtkMessageDialog, GtkAboutDialog, GtkFileChooserDialog, GtkFontSelectionDialog, GtkClorSelectionDialog를 다룰 것이다.

메시지 대화상자

메시지 대화상자를 이용하면 네 가지의 유용한 메시지들 중 하나를 제공할 수 있는데, 이러한 메시지로는 일반 정보 메시지, 오류 메시지, 경고 메시지, 질문 메시지가 있다. 대화상자의 유형은 표시해야 할 아이콘, 대화상자의 제목, 추가해야 할 버튼을 결정 시 사용된다.


메시지 내용과 관련해 어떤 가정도 하지 않는 일반적인 유형도 제공된다. 대부분의 경우에는 이를 사용하지 않을 것인데, 제공되는 네 가지 유형만으로도 대부분의 요구를 충족할 것이기 때문이다.


GtkMessageDialog 위젯을 재생성하기는 매우 간단하다. 첫 번째 두 예제는 간단한 메시지 대화상자를 구현했지만 GtkMessageDialog는 이미 이러한 기능을 제공하기 때문에 위젯을 재생성하지 않아도 된다. GtkMessageDialog를 이용하면 타이핑할 내용을 줄여주고 위젯을 여러 번 재생성해야 하는 필요가 없어지는데, 대부분의 애플리케이션은 GtkMessageDialog를 많이 사용하기 때문이다. 모든 GTK+ 애플리케이션에 걸쳐 메시지 대화상자를 획일화된 모양(uniform look)으로 제공하기도 한다.


그림 5-3은 버튼의 clicked 시그널의 시각적 알림을 사용자에게 제공할 때 사용되는 GtkMessageDialog(그림 5-1에 비교)의 예제를 나타낸다.

그림 5-3. GtkMessageDialog 위젯


메시지의 내용은 별로 중요하지 않으므로 그 타입은 일반 메시지로 설정된다. 이러한 메시지 대화상자는 리스팅 5-4에 실린 코드를 이용해 생성할 수 있다.


리스팅 5-4. GtkMessage 대화상자 이용하기 (messagedialogs.c)

#include <gtk/gtk.h>

static void button_clicked (GtkButton*, GtkWindow*);

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

    gtk_init (&argc, &argv);

    window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
    gtk_window_set_title (GTK_WINDOW (window), "Message Dialogs");
    gtk_container_set_border_width (GTK_CONTAINER (window), 10);

    button = gtk_button_new_with_mnemonic ("_Click Me");

    g_signal_connect (G_OBJECT (button), "clicked",
        G_CALLBACK (button_clicked),
        (gpointer) window);

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

    gtk_main ();
    return 0;
}

/* Create a new message dialog that tells the user that the button was clicked. */
static void
button_clicked (GtkButton *button,
        GtkWindow *parent)
{
    GtkWidget *dialog;

    dialog = gtk_message_dialog_new (parent, GTK_DIALOG_MODAL,
        GTK_MESSAGE_INFO, GTK_BUTTONS_OK,
        "The button was clicked!");
    gtk_window_set_title (GTK_WINDOW (dialog), "Information");

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


메인 창의 버튼을 클릭하면 위 예제는 gtk_message_dialog_new()를 이용해 새 GtkMessageDialog를 생성한다. 해당 함수에서 첫 번째 매개변수는 대화상자의 부모 GtkWindow가 된다.


부모 창은 필요 시 NULL로 설정 가능하지만, 대부분의 경우 부모-자식 관계가 구축되어야 한다. 부모 위젯을 설정하지 않으면 메시지 대화상자는 부모 창 위에서 중앙정렬(centered)되지 않을 것이다.


메시지 대화상자는 사용자가 즉시 다루도록 되어 있는데, 그 이유는 사용자의 주의를 요하는 중요한 질문이나 중요한 유형의 메시지를 제시하기 때문이다. 부모 창을 설정하지 않으면 메시지 대화상자는 쉽게 무시당하는데, 대부분의 경우 이는 바람직한 액션에 해당하지 않는다.

GtkWidget* gtk_message_dialog_new (GtkWindow *parent,
        GtkDialogFlags flags,
        GtkMessageType type,
        GtkButtonsType buttons,
        const gchar *message_format,
        ...);


다음으로 하나 또는 이상의 대화상자 플래그를 명시할 수 있다. 해당 매개변수에 대한 옵션은 앞의 세 예제에서 커스텀 대화상자를 생성 시 사용했던 GtkDialogFlags 목록에 의해 주어진다.


gtk_message_dialog_new()의 세 번째 매개변수는 당신이 생성하고자 하는 메시지 대화상자의 타입을 명시하는 데에 사용된다. 대화상자에 표시되는 이미지와 제목은 개발자가 선택하는 타입을 기반으로 설정된다. 가령, 리스팅 5-4에서는 GTK_MESSAGE_INFO 대화상자가 생성되었다. 따라서 lightbulb 이미지(GTK_STOCK_DIALOG_INFO)가 대화상자에 위치하고 제목은 "Information"으로 설정된다. GtkMessageType 목록에서 이용 가능한 다섯 가지 타입의 메시지는 다음과 같다.

  • GTK_MESSAGE_INFO: 사용자에게 정보를 제공하는 일반 메시지.
  • GTK_MESSAGE_WARNING: 치명적이지 않은 오류가 발생했다는 경고.
  • GTK_MESSAGE_QUESTION: 사용자의 선택을 요구하는 질문을 한다. 이러한 타입의 메시지에는 다수의 버튼이 제공되어야 한다.
  • GTK_MESSAGE_ERROR: 치명적인 오류가 발생했다는 경고.
  • GTK_MESSAGE_OTHER: 메시지의 내용과 관련해 어떤 추측도 하지 않는 일반적인 타입의 메시지.


다음으로 결정해야 할 사항은 대화상자에 어떤 타입의 버튼(들)을 표시할 것인지가 된다. 이러한 결정은 개발자가 생성한 메시지 타입을 기반으로 한다. 가령 GTK_MESSAGE_QUESTION을 타입으로 선택했다면 GTK_BUTTONS_YES_NO 또는 GTK_BUTTONS_OK_CANCEL을 선택하여 사용자가 질문에 대한 답을 제공할 수 있도록 하는 것이 논리적이겠다. 6 개의 가능한 GtkButtonsType 값의 리스트는 다음과 같다.

  • GTK_BUTTONS_NONE: 어떤 버튼도 추가되지 않을 것이다.
  • GTK_BUTTONS_OK: GTK_STOCK_OK 버튼을 추가한다.
  • GTK_BUTTONS_CLOSE: GTK_STOCK_CLOSE 버튼을 추가한다.
  • GTK_BUTONS_CANCEL: GTK_STOCK_CANCEL 버튼을 추가한다.
  • GTK_BUTTONS_YES_NO: GTK_STOCK_YES 버튼과 GTK_STOCK_NO 버튼을 추가한다.
  • GTK_BUTTONS_OK_CANCEL: GTK_STOCK_OK 버튼과 GTK_STOCK_CANCEL 버튼을 추가한다.


Gtkd note.png 대화상자 플래그는 GTK+의 다른 많은 열거 매개변수에 더해 bigwise 리스트가 되기도 하지만 GTK_MESSAGE_DIALOG에서 선택하는 버튼으로 동일한 일을 수행하는 것은 불가능하다. 이용 가능한 버튼 선택에 만족하지 않는다면 대화상자의 GtkHButtonBox 컨테이너에서 버튼을 제거하고 GtkDialog가 제공한 함수를 이용해 자신만의 버튼을 추가하라.


gtk_message_dialog_new()의 마지막 매개변수(또는 개발자의 요구에 따른 매개변수)는 대화상자에 의해 표시될 메시지다. 문자열은 printf()가 지원하는 것과 비슷하게 포맷팅되어야 한다. 이용 가능한 printf() 옵션에 대한 상세한 정보는 자신이 선호하는 C 언어 매뉴얼이나 서적을 참고해야 한다.


개발자는 gtk_message_dialog_new()로 제공되는 메시지의 시각적 포맷팅에 대해서는 어떠한 제어도 갖고 있지 않다. 메시지 대화상자의 텍스트를 포맷팅하는 데에 Pango Text Markup Language를 사용하고자 한다면 gtk_message_dialog_new_with_markup()을 이용해 대화상자를 생성할 수 있다. 이는 gtk_message_dialog_new()를 이용해 대화상자를 생성하고 gtk_message_dialog_set_markup()을 이용해 그 텍스트를 설정하는 것과 동일하다.

void gtk_message_dialog_set_format_secondary_text (GtkMessageDialog *dialog,
        const gchar *message_format,
        ...);


메시지 대화상자에 2차(secondary) 텍스트를 추가하는 것도 가능한데, 이런 경우 첫 번째 메시지는 gtk_message_dialog_set_format_secondary_text()를 이용해 볼드체로 설정될 것이다. 해당 함수로 제공된 텍스트 문자열은 printf()가 지원하는 포맷과 유사할 것이다.


해당 기능은 1차(primary) 텍스트를 빠르게 요약하고 2차 텍스트를 상세히 살펴보도록 해주기 때문에 매우 유용하다. gtk_message_dialog_set_format_secondary_markup()을 이용해 2차 텍스트의 markup을 설정할 수도 있다.


대화상자에 관하여

GtkAboutDialog 위젯은 사용자에게 애플리케이션에 관한 정보를 간단하게 제공할 수 있는 방법을 개발자를 위해 마련해준다. 이러한 대화상자는 Help 메뉴에서 GTK_STOCK_ABOUT 항목을 선택하면 표시되는 것이 보통이다. 하지만 메뉴는 제 9장에서 본격적으로 다룰 것이기 때문에 예제에 제시된 대화상자는 최상위 수준의 창으로서 사용될 것이다.


GtkAboutDialog를 이용해 표시할 수 있는 정보에는 많은 타입이 있다. 여기에는 애플리케이션 이름, 저작권, 현재 버전, 라이센스 내용, 저작자, 문서 작성자(documenter), 아티스트, 번역자가 포함된다. 모든 애플리케이션이 이 모든 사항을 가질 수는 없으므로 모든 프로퍼티는 선택적이다. 메인 창은 기본적인 정보만 표시하는데, 이는 그림 5-4에서 저작자 크레디트(author credits)와 함께 표시되어 있다.

그림 5-4. 대화상자와 저작자 크레디트


Credits 버튼을 클릭하면 사용자에게 저작자, 문서 작성자, 번역자, 아티스트가 표시될 것이다. 기부자(contributor)의 각 범주는 구분된 탭으로 표시된다.


License 버튼은 주어진 라이센스 내용을 표시하는 새 대화상자를 팝업시킬 것이다. 리스팅 5-5는 GtkAboutDialog 위젯에 이용 가능한 프로퍼티를 어떻게 사용하는지 보여주는 간단한 예제이다.


리스팅 5-5. GtkAboutDialog 이용하기 (aboutdialogs.c)

#include <gtk/gtk.h>

int main (int argc,
        char *argv[])
{
    GtkWidget *dialog;
    GdkPixbuf *logo;
    GError *error = NULL;

    gtk_init (&argc, &argv);

    const gchar *authors[] = {
        "Author #1",
        "Author #2",
        NULL
    };

    const gchar *documenters[] = {
        "Documenter #1",
        "Documenter #2",
        NULL
    };

    dialog = gtk_about_dialog_new ();

    /* You should edit '/path/to/logo.png' to point to the location of logo.png
    * from the chapter_5 source directory on your system. */
    logo = gdk_pixbuf_new_from_file ("/path/to/logo.png", &error);

    /* Set the application logo or handle the error. */
    if (error == NULL)
        gtk_about_dialog_set_logo (GTK_ABOUT_DIALOG (dialog), logo);
    else
    {
        if (error->domain == GDK_PIXBUF_ERROR)
            g_print ("GdkPixbufError: %s\n", error->message);
        else if (error->domain == G_FILE_ERROR)
            g_print ("GFileError: %s\n", error->message);
        else
            g_print ("An error in the domain: %d has occurred!\n", error->domain);

        g_error_free (error);
    }

    /* Set application data that will be displayed in the main dialog. */
    gtk_about_dialog_set_name (GTK_ABOUT_DIALOG (dialog), "GtkAboutDialog");
    gtk_about_dialog_set_version (GTK_ABOUT_DIALOG (dialog), "1.0");
    gtk_about_dialog_set_copyright (GTK_ABOUT_DIALOG (dialog),
        "(C) 2007 Andrew Krause");
    gtk_about_dialog_set_comments (GTK_ABOUT_DIALOG (dialog),
        "All About GtkAboutDialog");

    /* Set the license text, which is usually loaded from a file. Also, set the
    * web site address and label. */
    gtk_about_dialog_set_license (GTK_ABOUT_DIALOG (dialog), "Free to all!");
    gtk_about_dialog_set_website (GTK_ABOUT_DIALOG (dialog),
        "http://book.andrewkrause.net");
    gtk_about_dialog_set_website_label (GTK_ABOUT_DIALOG (dialog),
        "book.andrewkrause.net");

    /* Set the application authors, documenters and translators. */
    gtk_about_dialog_set_authors (GTK_ABOUT_DIALOG (dialog), authors);
    gtk_about_dialog_set_documenters (GTK_ABOUT_DIALOG (dialog), documenters);
    gtk_about_dialog_set_translator_credits (GTK_ABOUT_DIALOG (dialog),
        "Translator #1\nTranslator #2");

    gtk_dialog_run (GTK_DIALOG (dialog));
    gtk_widget_destroy (dialog);
    return 0;
}


자신만의 GtkAboutDialog 인스턴스를 생성할 때 설정할 수 있는 프로퍼티의 수는 많다. 표 5-2는 리스팅 5-5에 사용된 이러한 옵션들을 간추려 제공한다. 라이센스가 명시되지 않았다면 License 버튼은 보이지 않을 것이다. 크레디트가 없다면 Credits 버튼은 표시되지 않을 것이다.

옵션 설명
Name 애플리케이션의 이름.
Version 사용자가 실행 중인 애플리케이션의 현재 버전.
Copyright 하나 또는 두 개의 행 이상으로 이어질 수 없는 간략한 저작권 문자열.
Comments 하나 또는 두 개의 행 이상으로 이어질 수 없는 애플리케이션에 대한 간략한 설명.
License 2차 대화상자에 표시되는 라이센스 정보. 이를 NULL로 설정 시 License 버튼을 숨긴다.
Web Site 애플리케이션의 홈페이지 URL.
Web Site Label URL 대신 표시되는 라벨.
Authors 프로젝트에 코드를 기여한 저작자들의 NULL로 끝나는 배열.
Artists 프로젝트를 위해 그래픽을 생성해준 아티스트들의 NULL로 끝나는 배열.
Documenters 문서를 작성해준 문서 작성자들의 NULL로 끝나는 배열.
Translator Credits 현재 언어로 번역해준 번역자(들을)를 명시하는 문자열.
Logo 주로 파일로부터 로딩되며 이 GdkPixbuf 객체는 애플리케이션의 로고다.
표 5-2. GtkAboutDialog 옵션


저작자, 아티스트, 문서 작성자 크레디트와 달리 번역자 크레디트는 단일 문자열에 불과하다. 그 이유는 번역자 문자열은 현재 사용 중인 언어로 번역한 사람으로 설정되어야 하기 때문이다. 국제화와 gettext는 해당 서적에서 다루는 주제가 아니다. 더 많은 정보는 www.gnu.org/software/gettext를 참고한다.


GdkPixbuf

GdkPixbuf는 메모리에 저장된 이미지에 관한 정보를 포함하는 클래스다. 이는 모양이나 픽셀을 위치시킴으로써 이미지를 수동으로 빌드하거나 미리 빌드된 이미지를 파일로부터 로딩할 수 있도록 해준다. 대부분의 경우 파일에서 로딩하는 후자의 방법을 선호하므로, 본 저서에서도 이를 다루도록 하겠다.


GdkPixbuf는 GObject에서 파생되므로 참조(referencing)를 지원한다. 이는 g_object_ref()를 이용해 참조 계수를 증가시킴으로써 프로그램 내 여러 위치에서 동일한 이미지를 사용할 수 있다는 의미다. GdkPixbuf 객체(pixbufs)의 참조해제는 대부분의 경우 자동으로 실행된다.


파일로부터 pixbuf를 로딩하기 위해서는 리스팅 5-5에서 사용된 gdk_pixbuf_new_from_file()을 사용할 수 있다. 이 함수는 초기 크기가 이미지의 실제 크기로 설정된 이미지를 로딩할 것이다.

GdkPixbuf* gdk_pixbuf_new_from_file (const char *filename,
        GError **error);


이미지를 로딩한 후에는 gtk_pixbuf_scale_simple()을 이용해 크기를 조정할 수 있다. 해당 함수는 GdkPixbuf의 새로운 크기 매개변수들을 수락하고, 스케일링에 사용할 보간(interpolation) 모드를 수락한다.

GdkPixbuf* gdk_pixbuf_scale_simple (const GdkPixbuf *src,
        int destination_width,
        int destination_height,
        GdkInterpType interpolation);


네 가지의 GdkInterpType 모드는 아래와 같다.

  • GDK_INTERP_NEAREST: 샘플링이 가장 가까운 주변 픽셀에서 실행된다. 해당 모드는 매우 속도가 빠르지만 스케일링의 품질은 가장 낮다. 이미지를 작은 크기로 스케일링할 때는 절대 사용되어선 안 된다!
  • GDK_INTERP_TILES: 해당 모드는 모든 픽셀을 색상의 모양으로서 렌더링하고, 에지(edge)에 대한 안티 앨리어싱(anti-aliasing)을 사용한다. 이는 GDK_INTERP_NEAREST를 이용해 이미지를 크기 만들거나 GDK_INTERP_BILINEAR을 이용해 크기를 축소시키는 것과 비슷하다.
  • GDK_INTERP_BILINEAR: 이 모드는 이미지의 양방향 크기 조정에 최상의 모드인데, 이미지의 품질과 속도 간 균형을 유지하기 때문이다.
  • GDK_INTERP_HYPER: 품질은 매우 높은 반면 속도가 매우 느리다. 속도가 문제가 되지 않을 시에만 사용되어야 한다. 따라서 사용자가 빠른 디스플레이 시간을 기대하는 애플리케이션에는 절대 사용해선 안 된다. 편의상 gtk_pixbuf_new_from_file_at_size()라는 하나의 함수를 이용하면 파일로부터 이미지를 로딩한 직후 새로운 크기로 조정하는 일을 한꺼번에 실행할 수 있다.


GdkPixbuf 라이브러리에도 다른 많은 기능들이 제공되는데, 본문에서는 필요 시 몇 가지만 다룰 것이다. GdkPixbuf에 관한 더 많은 정보는 API 문서를 참고해야 한다.


GError

런타임 오류는 프로그래머라면 누구든 씨름해야 하는 문제다. 프로그래머에게 도움이 되고자 GLib는 오류 전파에 대해 GError 구조라고 불리는 표준 방법을 제공한다.

struct GError
{
    GQuark domain;
    gchar *message;
    gint code;
};


GError 구조는 3가지 값을 포함한다. domain 은 유사한 타입의 오류를 포함하는 그룹이다. 리스팅 5-5에서 우리는 GDK_PIXBUF_ERROR와 G_FILE_ERROR 도메인에서 오류를 검사한다.


Gtkd caution.png switch() 문에서 오류의 도메인을 확인할 것을 요청받을 것이다. 하지만 어떤 것도 작동하지 않을 것이니 확인하지 않아도 된다. 오류 도메인은 런타임 시 해결되므로 컴파일되지 않을 것인데, 이 시점에 case 문은 이미 결정되었기 때문이다.


message는 특정 오류가 발생했음을 설명하는 인간이 읽을 수 있는 문자열이다. 오류가 만일 사용자에게 시각적 피드백을 제공할 것을 요구할 경우 해당 메시지가 사용된다. 이 문자열은 당신이 g_error_free()를 호출하면 해제(free)된다.


마지막 요소인 code는 명시된 도메인에 해당하는 오류 코드다. 예를 들어 표 5-3은 GDK_PIXBUF_ERROR 도메인에서 발생할 수 있는 6 가지 타입의 오류를 표시한다. 이는 가능한 오류를 모두 표시한 리스트지만 GdkPixbuf 함수마다 오류가 모두 발생할 수 있는 것은 아니다.

오류 값 설명
GDK_PIXBUF_ERROR_CORRUPT_IMAGE 이미지 파일이 어떠한 식으로든 깨졌다.
GDK_PIXBUF_ERROR_INSUFFICIENT_MEMORY 이미지를 저장하는 데에 이용할 수 있는 메모리가 부족하다.
GDK_PIXBUF_ERROR_BAD_OPTION 올바르지 않은 옵션이 전달되었다. 해당 오류는 이미지를 저장 시 발생할 수 있다.
GDK_PIXBUF_ERROR_UNKNOWN_TYPE GdkPixbuf가 이미지 타입을 감지할 수 없었다.
GDK_PIXBUF_ERROR_UNSUPPORTED_OPERATION GdkPixbuf가 명시된 이미지에서 연산을 실행하는 방법을 알지 못한다.
GDK_PIXBUF_ERROR_FAILED 다른 모든 오류에 대한 일반적 실패 코드.
표 5-3. GdkPixbufError 목록 값


GLib는 오류 요소의 명명에 표준 타입을 이용한다. 오류 도메인은 항상 <NAMESPACE>_<MODULE>_ERROR를 이용해 포맷팅되는데, 네임스페이스는 함수를 포함하는 라이브러리에 해당하고, 모듈은 위젯이나 객체 타입에 해당한다.


도메인명의 끝에 오류 타입을 붙이면 오류 코드가 생성된다. 모든 오류 코드 목록마다 <NAMESPACE>_<MODULE>_ERROR_FAILED가 포함되어 있는데, 이는 호출되는 일반적인 실패 코드다. 특정 오류를 이용할 수 없는 경우 이것이 리턴될 것이다.


오류 코드를 확인하고 있다면 가장 발생하기 쉬운 것을 골라 선택해야 하는데, 모든 오류 타입을 확인하기란 효율적이지 않을 뿐더러 합리적이지도 않기 때문이다. 자신이 복구할 수 있는 오류 타입만 확인해야 한다. 그 외의 경우는 더 정밀한 사용자 피드백을 위해 인간이 읽을 수 있는 메시지가 제공된다.


GError 구조에는 파일링 업(piling up)이라 불리는 한 가지 단점이 발생한다. 두 개의 연속 함수에서 동일한 GError 구조를 사용할 경우 두 번째 오류가 첫 번째 오류를 대체할 것이란 점이다. 따라서 원본 오류는 영원히 손실된다.


이러한 문제를 예방하기 위해서는 함수를 처음으로 호출한 직후 오류를 처리해야 한다. 이후 g_clear_error()를 이용해 GError 구조 값을 초기 상태로 리셋한다. 이 시점에서 다음 함수에 GError 구조를 재사용할 수 있다.

if (error && * error)
{
    g_error_free (*error);
    *error = NULL;
}


g_clear_error()는 단순히 편의 함수여서 앞의 코드 조각에 표시된 기능을 수행한다는 점을 주목한다. 오류가 설정될 경우, g_error_free()를 호출하면 메시지 문자열을 먼저 해제한 후 GError 객체가 할당한 조각(slice)을 해제한다. 이후 오류를 NULL로 가리킨다.


GTK+와 그것이 지원하는 라이브러리에서 이용 가능한 전체 오류 도메인 리스트와 그에 상응하는 오류 타입은 부록 E에서 찾을 수 있다.


파일 선택자 대화상자

앞 장을 통해 GtkFileChooser와 GtkFileChooserButton 위젯을 학습한 바 있다. GtkFileChooser는 위젯이 아니라 인터페이스라는 점을 상기시켜보자. 인터페이스는 클래스와는 다른데, 인터페이스에서는 파생이 불가능하며 인터페이스 자체가 선언하는 기능을 구현하지 않기 때문이다.


GTK+는 GtkFileChooser 인터페이스를 구현하는 다음 세 가지 위젯을 제공한다.

  • GtkFileChooserButton: 파일 선택자 버튼은 앞 절에서 다루었다. 이는 클릭 시 GtkFileChooser 대화상자를 표시함으로써 사용자가 하나의 파일이나 폴더를 선택하도록 해준다.
  • GtkFileChooserDialog: 해당 위젯은 GtkFileChooserWidget을 자식 위젯으로서 사용하는 대화상자에 불과하다. 이는 GtkFileChooser 인터페이스를 구현하므로, 개발자가 그 자식 위젯으로 직접 접근하지 않아도 된다.
  • GtkFileChooserWidget: 사용자가 하나의 파일이나 폴더를 선택하도록 해주는 실제 위젯이다. 파일의 저장 또는 폴더의 생성을 가능하게 하기도 한다. GtkFileChooserDialog를 사용할 경우 사실상 GtkDialog에 패킹된 파일 선택자 위젯을 사용하는 것이다.


GtkFileChooserButton에 관해 이미 학습했으며 파일 선택자를 이용해 하나의 파일을 열거나 디렉터리를 선택하였다. 파일 선택자 위젯이 제공하는 기능에는 세 가지가 더 있다. 다음 세 개의 예제를 통해 우리는 파일 선택자 대화상자를 이용해 파일을 저장하고, 디렉터리를 생성하며, 다수의 파일을 선택하는 방법을 익힐 것이다.


파일 저장하기

그림 5-5는 파일을 저장하는 데에 사용되는 GtkFileChooserDialog 위젯을 보여준다. 다음에 소개할 두 개의 위젯과 모양이 비슷함을 눈치챌 것인데, 파일 선택자 대화상자의 타입은 모두 동일한 모양을 갖고 있어서 새로운 사용자의 혼동을 최소한으로 줄이고 효율성을 극대화시키기 때문이다. 위젯은 필요한 코드량을 최소화하기 위해 동일한 코드를 이용해 각 타입을 구현하기도 한다.

그림 5-5. 파일 저장에 사용되는 파일 선택자 대화상자


파일 선택자 대화상자는 본 장의 앞에서 다룬 두 개의 대화상자와 동일한 방식으로 사용되는데, gtk_dialog_new()를 이용해 당신이 응답 코드를 처리해야 한다는 점에서 차이가 있다. 리스팅 5-6은 사용자가 파일명을 선택하고, 응답 식별자가 리턴되면 버튼의 텍스트를 그 파일명으로 설정하도록 해준다.


리스팅 5-6. GtkFileChooserDialog을 이용해 파일 저장하기 (savefile.c)

#include <gtk/gtk.h>

static void button_clicked (GtkButton*, GtkWindow*);

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

    gtk_init (&argc, &argv);

    window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
    gtk_window_set_title (GTK_WINDOW (window), "Save a File");
    gtk_container_set_border_width (GTK_CONTAINER (window), 10);

    gtk_widget_set_size_request (window, 200, 100);

    button = gtk_button_new_with_label ("Save As ...");

    g_signal_connect (G_OBJECT (button), "clicked",
        G_CALLBACK (button_clicked),
        (gpointer) window);

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

    gtk_main ();
    return 0;
}

/* Allow the user to enter a new file name and location for
 the file and
* set the button to the text of the location. */
static void
button_clicked (GtkButton *button,
        GtkWindow *window)
{
    GtkWidget *dialog;
    gchar *filename;

    dialog = gtk_file_chooser_dialog_new ("Save File As ...", window,
        GTK_FILE_CHOOSER_ACTION_SAVE,
        GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
        GTK_STOCK_SAVE, GTK_RESPONSE_ACCEPT,
        NULL);

    gint result = gtk_dialog_run (GTK_DIALOG (dialog));
    if (result == GTK_RESPONSE_ACCEPT)
    {
        filename = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (dialog));
        gtk_button_set_label (button, filename);
    }

    gtk_widget_destroy (dialog);
}


개발자가 선택하는 옵션과 상관없이 모든 파일 선택자 대화상자는 gtk_file_chooser_dialog_new()를 이용해 생성된다. 다른 대화상자와 마찬가지로 대화상자와 부모 창의 제목을 설정함으로써 시작한다. 부모 창은 항상 설정되어야 하는데, 파일 선택자 대화상자는 모드형이기 때문이다.

GtkWidget* gtk_file_chooser_dialog_new (const gchar *title,
        GtkWindow *parent,
        GtkFileChooserAction action,
        const gchar *first_button_text,
        ...);


다음으로, 파일 선택자 버튼과 마찬가지로 개발자는 생성되어야 할 파일 선택자의 액션을 선택해야 한다. GtkFileChooser 인터페이스가 제공하는 네 가지 액션 타입을 GtkFileChooserDialog에서 이용할 수 있는데, 그 타입은 다음과 같다.

  • GTK_FILE_CHOOSER_ACTION_SAVE: 사용자는 파일명을 입력하고 파일 시스템에서 위치를 찾아볼 것을 요청 받는다. 리턴된 파일은 끝에 새 파일명이 붙은 선택된 경로가 될 것이다. 사용자가 이미 존재하는 파일명을 입력하면 GtkFileChooser는 확인을 요청할 수 있는 방법을 제공한다.
  • GTK_FILE_CHOOSER_ACTION_OPEN: 파일 선택자는 사용자가 사용자의 시스템에 이미 존재하는 하나 또는 이상의 파일만 선택하도록 해줄 것이다. 사용자는 파일 시스템을 브라우징하거나 북마크 해둔 위치를 선택할 수 있을 것이다.
  • GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER: 파일 선택자는 사용자에게 이미 존재하는 폴더만 선택하도록 해줄 것이다. 사용자는 폴더만 선택할 수 있으므로 파일 시스템에 있는 다른 파일들은 표시되지 않을 것이다.
  • GTK_FILE_CHOOSER_ACTION_CREATE_FOLDER: 사용자가 위치를 선택하고 새 폴더명을 명시하도록 해주기 때문에 저장 액션과 매우 유사하다. 사용자는 Create Folder 버튼을 클릭하거나 파일 선택자가 리턴 시 생성되는 새 폴더명을 입력하면 그림 5-6와 같이 현재 디렉터리에 새 폴더가 생성될 것이다.


마지막으로 개발자는 응답 식별자와 함께 액션 영역에 추가될 NULL로 끝나는 버튼의 리스트를 제공해야 한다. 리스팅 5-6에서 Cancel 버튼을 클릭하면 GTK_RESPONSE_CANCEL이 방출되고, Save 버튼을 클릭하면 GTK_RESPONSE_ACCEPT가 방출된다.


폴더 생성하기

GTK+는 폴더를 선택하게 해줄 뿐만 아니라 폴더의 생성도 허용한다. 이러한 타입을 이용한 GtkFileChooserDialog 위젯은 리스팅 5-7의 스크린샷에 해당하는 그림 5-6에서 확인할 수 있다.

그림 5-6. 폴더 생성에 사용되는 파일 선택자 대화상자


리스팅 5-7의 대화상자는 사용자가 수락 시 새로운 폴더의 생성을 처리하므로, 대화상자의 소멸 이외에는 어떠한 액션도 취할 필요가 없다.


리스팅 5-7. GtkFileChooserDialog를 이용해 폴더 사용하기 (createfolder.c)

#include <gtk/gtk.h>

int main (int argc,
        char *argv[])
{
    GtkWidget *dialog;
    gchar *filename;
    gint result;

    gtk_init (&argc, &argv);

    /* Create a new GtkFileChooserDialog that will be used to create a new folder. */
    dialog = gtk_file_chooser_dialog_new ("Create a Folder ...", NULL,
        GTK_FILE_CHOOSER_ACTION_CREATE_FOLDER,
        GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
        GTK_STOCK_OK, GTK_RESPONSE_OK,
        NULL);

    result = gtk_dialog_run (GTK_DIALOG (dialog));
    if (result == GTK_RESPONSE_OK)
    {
        filename = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (dialog));
        g_print ("Creating directory: %s\n", filename);
    }

    gtk_widget_destroy (dialog);
    return 0;
}


대화상자의 전체 폴더명은 앞의 예제에서 파일명을 검색한 함수와 동일한 함수 gtk_file_chooser_get_filename()를 이용해 검색할 수 있다. 표준 GLib 함수 g_mkdir()는 모든 지원되는 운영체제에서 명시된 위치에 폴더를 생성할 것이다.


다중 파일 선택하기

그림 5-7은 사용자에게 파일의 선택을 허용하는 표준 파일 선택자 대화상자를 표시한다. GTK_FILE_CHOOSE_ACTION_OPEN 타입을 이용하는 GtkFileChooserButton과 GtkFileChooserDialog의 차이는, 버튼의 경우 하나의 파일로 제한되는 반면 대화상자는 다중 파일을 선택할 수 있는 기능이 있다는 점을 들 수 있겠다.

그림 5-7. 다중 파일 선택에 사용되는 파일 선택자 대화상자


리스팅 5-8은 다중 파일 선택을 처리하는 방법을 보여준다. 이는 선택이 단일 연결 리스트로 리턴된다는 사실만 제외하면 단일 파일 선택과 매우 비슷하다.


리스팅 5-8. GtkFileChooserDialog를 이용해 다중 파일 선택하기 (multiplefiles.c)

static void
button_clicked (GtkButton *button,
        GtkWindow *window)
{
    GtkWidget *dialog;
    GSList *filenames;

    dialog = gtk_file_chooser_dialog_new ("Open File(s) ...", window,
        GTK_FILE_CHOOSER_ACTION_OPEN,
        GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
        GTK_STOCK_OPEN, GTK_RESPONSE_ACCEPT,
        NULL);

    /* Allow the user to choose more than one file at a time. */
    gtk_file_chooser_set_select_multiple (GTK_FILE_CHOOSER (dialog), TRUE);

    gint result = gtk_dialog_run (GTK_DIALOG (dialog));

    if (result == GTK_RESPONSE_ACCEPT)
    {
        filenames = gtk_file_chooser_get_filenames (GTK_FILE_CHOOSER (dialog));

        while (filenames != NULL)
        {
            gchar *file = (gchar*) filenames->data;
            g_print ("%s was selected.\n", file);
            filenames = filenames->next;
        }
    }

    gtk_widget_destroy (dialog);
}


gtk_file_chooser_get_filenames() 함수는 단일 연결 리스트인 GSList라고 불리는 새로운 GLib 데이터 타입을 리턴한다. 이는 한 방향으로만 반복할 수 있는 연결 리스트들이다. 리스트 내 각 요소는 데이터 조각과 다음 요소로의 링크를 포함한다.

gchar *file = (gchar*) filenames->data;


GLib 내의 연결 리스트는 데이터를 gpointers로 저장하므로 모든 데이터 타입이 저장될 수 있다. 이 때문에 g_slist_nth_data()로부터 리턴된 데이터는 본래 데이터 타입으로서 형변환되어야 한다. 리스트 내 첫 번째 요소는 0으로 색인된다.


GSList 구조는 길이의 검색, 요소를 뒤에 추가, 앞에 추가, 삽입, 제거하는 데 필요한 함수를 제공하기도 한다. 단일 및 이중 연결 리스트에 관한 더 많은 정보는 다음 장에서 찾을 수 있다.


색상 선택 대화상자

앞 장에서는 사용자가 색상을 선택하도록 해주는 GtkColorButton 위젯에 관해 학습하였다. 사용자가 그 버튼을 클릭하면 대화상자가 표시된다. 당시엔 명시하지 않았으나 그 대화상자는 GtkColorSelectionDialog 위젯이었다.


색상 선택 대화상자는 GtkFileChooserDialog와 비슷하며, 사실상 GtkDialog 컨테이너에 GtkColorSelection 위젯이 자식 위젯으로서 패킹된 것이다. GtkColorSelection은 쉽게 독립적으로 사용 가능하다. 하지만 대화상자는 위젯을 표시하는 자연스러운 방식이기 때문에 GTK+는 GtkColorSelectionDialog를 제공한다. 색상 선택 대화상자는 그림 5-8에서 표시한다.

그림 5-8. 색상 선택 대화상자


리스팅 5-9는 두 개의 버튼이 있는 최상위 수준의 창을 포함한다. 첫 번째 버튼을 클릭하면 모드형 GtkColorSelectionDialog가 생성된다. 나머지 버튼은 비모드형 GtkColorSelectionDialog를 생성할 것이다. 각 버튼은 전역 색상 값과 불투명도 값을 선택하는 데에 사용된다.


해당 예제는 프로그램 인자를 반복하면서 초기 색상 값이 제공 시 그 값을 설정한다. 따라서 애플리케이션을 시작하면 개발자는 초기 값을 전달할 수 있다.


리스팅 5-9. GtkColorSelectionDialog 이용하기 (colorselection.c)

#include <gtk/gtk.h>

static void run_color_selection_dialog (GtkButton*, GtkWindow*, gboolean);
static void modal_clicked (GtkButton*, GtkWindow*);
static void nonmodal_clicked (GtkButton*, GtkWindow*);
static void dialog_response (GtkDialog*, gint, gpointer);

static GdkColor global_color;
static guint global_alpha = 65535;

int main (int argc,
        char *argv[])
{
    GtkWidget *window, *hbox, *modal, *nonmodal;
    gint i;

    gtk_init (&argc, &argv);

    /* Loop through the parameters. The first color name that is specified and
    * successfully parsed, it will be used as the initial color of the selection. */
    for (i=1; i < argc; i++)
        if (gdk_color_parse (argv[i], &global_color))
            break;

    window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
    gtk_window_set_title (GTK_WINDOW (window), "Color Selection Dialogs");
    gtk_container_set_border_width (GTK_CONTAINER (window), 10);
    gtk_widget_set_size_request (window, 200, 75);

    modal = gtk_button_new_with_label ("Modal");
    nonmodal = gtk_button_new_with_label ("Non-Modal");

    g_signal_connect (G_OBJECT (modal), "clicked",
        G_CALLBACK (modal_clicked),
        (gpointer) window);
    g_signal_connect (G_OBJECT (nonmodal), "clicked",
        G_CALLBACK (nonmodal_clicked),
        (gpointer) window);

    hbox = gtk_hbox_new (TRUE, 10);
    gtk_box_pack_start_defaults (GTK_BOX (hbox), modal);
    gtk_box_pack_start_defaults (GTK_BOX (hbox), nonmodal);

    gtk_container_add (GvtTK_CONTAINER (window), hbox);
    gtk_widget_show_all (window);

    gtk_main ();
    return 0;
}

/* Create a new color selection dialog that is modal. */
static void
modal_clicked (GtkButton *button,
        GtkWindow *window)
{
    run_color_selection_dialog (button, window, TRUE);
}

/* Create a new color selection dialog that is nonmodal. */
static void
nonmodal_clicked (GtkButton *button,
        GtkWindow *window)
{
    run_color_selection_dialog (button, window, FALSE);
}

/* Create a new color selection dialog and allow the user to choose a color
* and an opacity value. */
static void
run_color_selection_dialog (GtkButton *button,
        GtkWindow *window,
        gboolean domodal)
{
    GtkWidget *dialog, *colorsel;
    gchar *title;

    if (domodal)
        title = "Choose Color -- Modal";
    else
        title = "Choose Color -- Non-Modal";

    dialog = gtk_color_selection_dialog_new (title);
    gtk_window_set_modal (GTK_WINDOW (dialog), domodal);

    colorsel = GTK_COLOR_SELECTION_DIALOG (dialog)->colorsel;
    gtk_color_selection_set_has_opacity_control (GTK_COLOR_SELECTION (colorsel),
        TRUE);

    gtk_color_selection_set_current_color (GTK_COLOR_SELECTION (colorsel),
        &global_color);
    gtk_color_selection_set_current_alpha (GTK_COLOR_SELECTION (colorsel),
        global_alpha);

    g_signal_connect (G_OBJECT (dialog), "response",
        G_CALLBACK (dialog_response), NULL);
    gtk_widget_show_all (dialog);
}

/* Handle the response identifier from the assistant. Either tell the user to
* read the manual, retrieve the new color value or destroy the dialog. */
static void
dialog_response (GtkDialog *dialog,
        gint result,
        gpointer data)
{
    GtkWidget *colorsel;
    GdkColor color = { 0, };
    guint16 alpha = 0;

    switch (result)
    {
    case GTK_RESPONSE_HELP:
        g_print("Read the GTK+ API documentation.\n");
        break;

    case GTK_RESPONSE_OK:
        colorsel = GTK_COLOR_SELECTION_DIALOG (dialog)->colorsel;
        alpha = gtk_color_selection_get_current_alpha (GTK_COLOR_SELECTION (colorsel));
        gtk_color_selection_get_current_color (GTK_COLOR_SELECTION (colorsel), &color);

        g_print ("#%04X%04X%04X%04X\n", color.red, color.green, color.blue, alpha);

        global_color = color;
        global_alpha = alpha;

    default:
        gtk_widget_destroy (GTK_WIDGET(dialog));
    }
}


GtkColorSelectionDialog 클래스가 제공하는 유일한 함수는 gtk_color_selection_dialog_new()로서, 명시된 제목으로 된 새로운 색상 선택 대화상자를 리턴할 것이다.

struct GtkColorSelectionDialog
{
    GtkWidget *colorsel;
    GtkWidget *ok_button;
    GtkWidget *cancel_button;
    GtkWidget *help_button;
};


GtkColorSelectionDialog는 네 개의 이용 가능한 자식 위젯으로 직접 접근을 제공한다. 먼저 colorsel은 색상 선택을 가능하게 하는 GtkColorSelection 위젯이다. 나머지 세 개는 GTK_STOCK_OK, GTK_STOCK_CANCEL, GTK_STOCK_HELP 버튼이다. 기본적으로 Help 버튼은 숨겨진다. 이를 표시하려면 gtk_widget_show()를 이용할 수 있다.


리스팅 5-2와 같이 이 예제는 response 시그널로 연결하는데, 이는 대화상자가 모드형이든 비모드형이든 상관없이 응답 식별자를 모두 수신하는 데에 사용된다. 대화상자는 gtk_window_set_modal()을 이용해 모드형 또는 비모드형으로 설정된다.


리스팅 5-9는 RGB 값을 제외하고 네 번째 색상 프로퍼티, 그 불투명도(알파 값)를 보여준다. 0-65,535 범위에 있는 이 값은 색상을 얼마나 투명하게 그릴 것인지를 조절하는데, 0은 완전히 투명하고 65,535은 완전히 불투명하다. 기본적으로 불투명도 조절은 색상 선택 위젯에서 꺼져 있다. 해당 기능을 활성화하려면 gtk_color_selection_set_has_opacity_control() 함수를 호출하면 된다.

void gtk_color_selection_set_has_opacity_control (GtkColorSelection *colorsel,
        gboolean has_opacity);


불투명도 기능이 켜지면 16진 색상 값은 16 자릿수 길이로, 각 값마다 4자릿수 길이인데, 각 값은 적색, 녹색, 청색, 알파 값이 해당한다. 불투명도는 GdkColor 구조에 보관되지 않으므로 색상 선택 위젯으로부터 그 값을 검색하기 위해서는 gtk_color_selection_get_current_alpha()를 이용해야 한다.

g_print ("#%04X%04X%04X%04X\n", color.red, color.green, color.blue, alpha);


글꼴 선택 대화상자

글꼴 선택 대화상자는 사용자가 글꼴을 선택하도록 해주며, GtkFontButton 버튼을 클릭 시 표시되는 대화상자다. GtkColorSelectionDialog와 마찬가지로 액션 영역 버튼에 대한 직접 접근은 GtkFontSelectionDialog 구조를 통해 제공된다. 글꼴 선택 대화상자의 예제는 그림 5-9에서 살펴볼 수 있는데, 앞 장에서 살펴본 내용과 비슷할 것이다.

그림 5-9. 글꼴 선택 대화상자


리스팅 5-10에서는 GtkFontSelectionDialog를 최상위 수준 위젯으로서 사용한다. 해당 대화상자는 예제에서 최상위 수준의 창으로서 사용되며 어떤 대화상자에도 가능함을 주목해야 한다. 하지만 여기에 익숙해지면 안 되는 것이, 해당 방법이 가능하긴 하지만 바람직하지는 않은 프로그래밍 실습이기 때문이다.


리스팅 5-10. GtkFontSelectionDialog (fontselection.c)

#include <gtk/gtk.h>

static void ok_clicked (GtkButton*, GtkWidget*);
static void font_dialog_response (GtkFontSelectionDialog*, gint, gpointer);

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

    gtk_init (&argc, &argv);

    /* Use the font selection dialog as the top-level widget. */
    dialog = gtk_font_selection_dialog_new ("Choose a Font");
    gtk_font_selection_dialog_set_font_name (GTK_FONT_SELECTION_DIALOG (dialog),
        "Sans Bold Italic 12");

    gtk_font_selection_dialog_set_preview_text (GTK_FONT_SELECTION_DIALOG (dialog),
        "Foundations of GTK+ Development");

    g_signal_connect (G_OBJECT (dialog), "response",
        G_CALLBACK (font_dialog_response), NULL);

    gtk_widget_show_all (dialog);

    gtk_main ();
    return 0;
}

/* If the user clicks "Apply", display the font, but do not destroy the dialog. If
* "OK" is pressed, display the font and destroy the dialog. Otherwise, just destroy
* the dialog. */
static void
font_dialog_response (GtkFontSelectionDialog *dialog,
        gint response,
        gpointer data)
{
    gchar *font;
    GtkWidget *message;

    switch (response)
    {
    case (GTK_RESPONSE_APPLY):
    case (GTK_RESPONSE_OK):
        font = gtk_font_selection_dialog_get_font_name (dialog);
        message = gtk_message_dialog_new (NULL, GTK_DIALOG_MODAL,
            GTK_MESSAGE_INFO, GTK_BUTTONS_OK, font);
        gtk_window_set_title (GTK_WINDOW (message), "Selected Font");

        gtk_dialog_run (GTK_DIALOG (message));
        gtk_widget_destroy (message);
        g_free (font);
        break;
    default:
        gtk_widget_destroy (GTK_WIDGET (dialog));
    }

    if (response == GTK_RESPONSE_OK)
        gtk_widget_destroy (GTK_WIDGET (dialog));
}


글꼴 선택 대화상자의 초기화 함수 gtk_font_selection_dialog_new()는 명시된 제목으로 된 새 GtkFontSelectionDialog 위젯을 리턴한다.

struct GtkFontSelectionDialog
{
    GtkWidget *ok_button;
    GtkWidget *apply_button;
    GtkWidget *cancel_button;
};


대화상자 자체에는 세 개의 버튼, GTK_STOCK_OK, GTK_STOCK_APPLY, GTK_STOCK_CANCEL이 포함되어 있다.


글꼴 선택 대화상자는 이미 최상위 수준의 위젯이므로 모드형 대화상자를 생성할 필요가 없다. 따라서 대화상자는 response 시그널로 연결된다.


사용자가 OK 버튼을 클릭하면 사용자에게 선택된 글꼴이 표시되고, 대화상자는 소멸된다. Apply 버튼을 클릭하면 선택된 글꼴이 사용자에게 표시되지만 대화상자는 소멸되지 않는다. 이 때 개발자는 새 글꼴을 적용시킬 수 있기 때문에 추후 사용자는 대화상자를 닫지 않고 변경내용을 확인할 수 있다.


글꼴 선택 위젯에는 사용자로 하여금 글꼴의 미리보기를 가능하게 해주는 GtkEntry 위젯이 포함되어 있다. 미리보기 텍스트는 "abcdefghijk ABCDEFGHIJK"을 기본값으로 한다. 텍스트가 다소 지루한 점이 있기 때문에 필자는 본 책의 제목을 본따 "Foundations of GTK+ Development"로 리셋하기로 결정했다.


GtkFontSelectionDialog가 제공하는 마지막 함수들은 현재 글꼴 문자열을 설정 및 검색하도록 해준다. gtk_font_selection_dialog_set_font_name()과 gtk_font_selection_dialog_get_font_name()이 사용하는 글꼴 문자열은 앞 장에서 PangoFontDescription으로 파싱했던 포맷과 동일한 포맷을 한다.


여러 페이지가 있는 대화상자

GTK+ 2.10 배포판에서는 GtkAssistant라는 위젯이 도입되었는데, 이는 전체 대화상자를 프로그램적으로 생성할 필요가 없기 때문에 여러 페이지가 있는 대화상자를 쉽게 생성하도록 해준다. 이는 복잡한 대화상자를 단계로 나누어 사용자를 안내해준다. 해당 기능은 다양한 애플리케이션에서 마법사라고 불리는 것으로 구현된다.

그림 5-10. GtkAssistant 위젯의 첫 페이지


그림 5-10은 간단한 GtkAssistant 위젯의 첫 페이지를 보여주는데, 리스팅 5-11에 실린 코드를 이용해 생성된다. 해당 예제는 사용자 일반 정보를 제공함으로써 시작된다. 다음 페이지는 GtkEntry 위젯에 텍스트가 입력될 때까지 사용자가 넘어가지 못할 것이다. 세 번째 페이지는 GtkCheckButton 버튼이 활성화될 때까지 사용자가 진행하지 못하도록 할 것이다. 네 번째 페이지는 진행 막대가 채워질 때까지 어떤 일도 실행할 수 없을 것이며, 다섯 번째 페이지는 어떤 일이 일어났는지에 대해 개요를 제공한다. 이는 GtkAssistant 위젯이라면 모두 준수해야 하는 일반적인 흐름이다.


리스팅 5-11. GtkAssistant 위젯 (assistant.c)

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

static void entry_changed (GtkEditable*, GtkAssistant*);
static void button_toggled (GtkCheckButton*, GtkAssistant*);
static void button_clicked (GtkButton*, GtkAssistant*);
static void assistant_cancel (GtkAssistant*, gpointer);
static void assistant_close (GtkAssistant*, gpointer);

typedef struct {
    GtkWidget *widget;
    gint index;
    const gchar *title;
    GtkAssistantPageType type;
    gboolean complete;
} PageInfo;

int main (int argc,
        char *argv[])
{
    GtkWidget *assistant, *entry, *label, *button, *progress, *hbox;
    guint i;
    PageInfo page[5] = {
        { NULL, -1, "Introduction", GTK_ASSISTANT_PAGE_INTRO, TRUE},
        { NULL, -1, NULL, GTK_ASSISTANT_PAGE_CONTENT, FALSE},
        { NULL, -1, "Click the Check Button", GTK_ASSISTANT_PAGE_CONTENT,
 FALSE},
        { NULL, -1, "Click the Button", GTK_ASSISTANT_PAGE_PROGRESS, FALSE},
        { NULL, -1, "Confirmation", GTK_ASSISTANT_PAGE_CONFIRM, TRUE},
    };

    gtk_init (&argc, &argv);

    /* Create a new assistant widget with no pages. */
    assistant = gtk_assistant_new ();
    gtk_widget_set_size_request (assistant, 450, 300);
    gtk_window_set_title (GTK_WINDOW (assistant), "GtkAssistant Example");

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

    page[0].widget = gtk_label_new ("This is an example of a GtkAssistant. By\n"\
        "clicking the forward button, you can continue\n"\
        "to the next section!");
    page[1].widget = gtk_hbox_new (FALSE, 5);
    page[2].widget = gtk_check_button_new_with_label ("Click Me To Continue!");
    page[3].widget = gtk_alignment_new (0.5, 0.5, 0.0, 0.0);
    page[4].widget = gtk_label_new ("Text has been entered in the label and the\n"\
        "combo box is clicked. If you are done, then\n"\
        "it is time to leave!");

    /* Create the necessary widgets for the second page. */
    label = gtk_label_new ("Your Name: ");
    entry = gtk_entry_new ();
    gtk_box_pack_start (GTK_BOX (page[1].widget), label, FALSE, FALSE, 5);
    gtk_box_pack_start (GTK_BOX (page[1].widget), entry, FALSE, FALSE, 5);

    /* Create the necessary widgets for the fourth page. The, Attach the progress bar
    * to the GtkAlignment widget for later access.*/
    button = gtk_button_new_with_label ("Click me!");
    progress = gtk_progress_bar_new ();
    hbox = gtk_hbox_new (FALSE, 5);
    gtk_box_pack_start (GTK_BOX (hbox), progress, TRUE, FALSE, 5);
    gtk_box_pack_start (GTK_BOX (hbox), button, FALSE, FALSE, 5);
    gtk_container_add (GTK_CONTAINER (page[3].widget), hbox);
    g_object_set_data (G_OBJECT (page[3].widget), "pbar", (gpointer) progress);

    /* Add five pages to the GtkAssistant dialog. */
    for (i = 0; i < 5; i++)
    {
        page[i].index = gtk_assistant_append_page (GTK_ASSISTANT (assistant),
            page[i].widget);
        gtk_assistant_set_page_title (GTK_ASSISTANT (assistant),
            page[i].widget, page[i].title);
        gtk_assistant_set_page_type (GTK_ASSISTANT (assistant),
            page[i].widget, page[i].type);

        /* Set the introduction and conclusion pages as complete so they can be
        * incremented or closed. */
        gtk_assistant_set_page_complete (GTK_ASSISTANT (assistant),
            page[i].widget, page[i].complete);
    }

    /* Update whether pages 2 through 4 are complete based upon whether there is
    * text in the GtkEntry, the check button is active, or the progress bar
    * is completely filled. */
    g_signal_connect (G_OBJECT (entry), "changed",
        G_CALLBACK (entry_changed), (gpointer) assistant);
    g_signal_connect (G_OBJECT (page[2].widget), "toggled",
        G_CALLBACK (button_toggled), (gpointer) assistant);
    g_signal_connect (G_OBJECT (button), "clicked",
        G_CALLBACK (button_clicked), (gpointer) assistant);

    g_signal_connect (G_OBJECT (assistant), "cancel",
        G_CALLBACK (assistant_cancel), NULL);
    g_signal_connect (G_OBJECT (assistant), "close",
        G_CALLBACK (assistant_close), NULL);

    gtk_widget_show_all (assistant);

    gtk_main ();
    return 0;
}

/* If there is text in the GtkEntry, set the page as complete. Otherwise,
* stop the user from progressing the next page. */
static void
entry_changed (GtkEditable *entry,
        GtkAssistant *assistant)
{
    const gchar *text = gtk_entry_get_text (GTK_ENTRY (entry));
    gint num = gtk_assistant_get_current_page (assistant);
    GtkWidget *page = gtk_assistant_get_nth_page (assistant, num);

    gtk_assistant_set_page_complete (assistant, page, (strlen (text) > 0));
}

/* If the check button is toggled, set the page as complete. Otherwise,
* stop the user from progressing the next page. */
static void
button_toggled (GtkCheckButton *toggle,
        GtkAssistant *assistant)

{
    gboolean active = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (toggle));
    gtk_assistant_set_page_complete (assistant, GTK_WIDGET (toggle), active);
}

/* Fill up the progress bar, 10% every second when the button is clicked. Then,
* set the page as complete when the progress bar is filled. */
static void
button_clicked (GtkButton *button,
        GtkAssistant *assistant)
{
    GtkProgressBar *progress;
    GtkWidget *page;
    gdouble percent = 0.0;

    gtk_widget_set_sensitive (GTK_WIDGET (button), FALSE);
    page = gtk_assistant_get_nth_page (assistant, 3);
    progress = GTK_PROGRESS_BAR (g_object_get_data (G_OBJECT (page), "pbar"));

    while (percent <= 100.0)
    {
        gchar *message = g_strdup_printf ("%.0f%% Complete", percent);
        gtk_progress_bar_set_fraction (progress, percent / 100.0);
        gtk_progress_bar_set_text (progress, message);

        while (gtk_events_pending ())
            gtk_main_iteration ();

        g_usleep (500000);
        percent += 5.0;
    }

    gtk_assistant_set_page_complete (assistant, page, TRUE);
}

/* If the dialog is cancelled, delete it from memory and then clean up after
* the Assistant structure. */
static void
assistant_cancel (GtkAssistant *assistant,
        gpointer data)

{
    gtk_widget_destroy (GTK_WIDGET (assistant));
}

/* This function is where you would apply the changes and destroy the assistant. */
static void
assistant_close (GtkAssistant *assistant,
        gpointer data)
{
    g_print ("You would apply your changes now!\n");
    gtk_widget_destroy (GTK_WIDGET (assistant));
}


GtkAssistant 페이지 생성하기

GtkAssistant 위젯은 여러 페이지가 있는 대화상자지만 사실상 GtkDialog에서 파생되지 않는다. gtk_assistant_new()를 호출하면 초기(initial) 페이지가 없는 새 GtkAssistant 위젯을 생성할 수 있다.

index = gtk_assistant_append_page (GTK_ASSISTANT (assistant), widget);


각 페이지는 사실상 gtk_assistant_prepend_page(), gtk_assistant_append_page(), gtk_assistant_insert_page() 중 하나를 이용해 추가되는 자식 위젯이기 때문에 assistant를 위한 실제 페이지는 존재하지 않는다. 이러한 각각의 함수는 페이지의 내용으로서 추가된 자식 위젯을 수락하고 새 페이지의 색인을 리턴한다. 각 페이지는 설정 가능한 수많은 프로퍼티를 갖고 있으며, 각각은 선택적이다. 이러한 옵션의 리스트는 다음과 같다.

  • 페이지 제목: 모든 페이지에는 제목이 있어야만 사용자가 그 목적을 이해할 수 있다. 자신의 첫 번째 페이지는 assistant에 관한 사용자 정보를 알려주는 소개 페이지여야 한다. 마지막 페이지는 사용자가 이전에 변경한 내용을 적용할 준비가 되도록 해주는 개요 또는 확인 페이지다.
  • 헤더 이미지: 상단 패널에 제목의 좌측으로 선택적 이미지를 표시할 수 있다. 이는 종종 애플리케이션의 로고 또는 이미지로, assistant의 목적을 보완한다.
  • 측면(side) 이미지. 이 선택적 이미지는 메인 페이지 내용 외에 assistant의 좌측면에 따라 위치한다. 미적 어필용으로 사용된다.
  • 페이지 타입: 페이지 타입은 꼭 설정되어야 하며, 그렇지 않을 경우 기본값은 GTK_ASSISTANT_PAGE_CONTENT로 설정된다. 마지막 페이지는 항상 확인 또는 개요 페이지여야 한다. 첫 번째 페이지는 소개 페이지로, assistant가 실행하는 업무에 관한 사용자 정보를 제공하도록 해야 한다.


페이지의 프로퍼티를 설정한 후에는 페이지 타입을 선택해야 한다. 페이지 타입에는 다섯 가지가 있다. 첫 페이지는 항상 GTK_ASSISTANT_PAGE_INTRO여야 한다. 마지막 페이지는 항상 GTK_ASSISTANT_PAGE_CONFIRM 또는 GTK_ASSISTANT_PAGE_SUMMARY가 되어야 하는데, assistant가 이 두 타입 중 하나로 끝나지 않을 경우 올바로 작동하지 않을 것이다. 이용 가능한 다섯 가지 페이지 타입은 다음과 같다.

  • GTK_ASSISTANT_PAGE_CONTENT: 일반 내용의 페이지 타입으로, assistant에서 거의 모든 페이지에 사용될 것을 의미한다. assistant의 마지막 페이지에는 사용해선 안 된다.
  • GTK_ASSISTANT_PAGE_INTRO: 해당 페이지 타입은 사용자를 위한 소개 정보를 포함한다. assistant에서 첫 번째 페이지에만 설정 가능하다. 소개 페이지는 의무적으로 포함해야 하는 것은 아니지만 사용자에게 방향을 제시하여 대부분의 assistant에서는 사용되어야 한다.
  • GTK_ASSISTANT_PAGE_CONFIRM: 사용자가 변경내용의 집합을 확인 또는 거부하도록 해준다. 주로 실행취소할 수 없는 변경내용에 사용되고, 올바로 설정되지 않으면 무언가 깨질 수 있다(break). assistant의 마지막 페이지에만 설정되어야 한다.
  • GTK_ASSISTANT_PAGE_SUMMARY: 해당 페이지는 발생한 변경내용에 대한 개요를 제공한다. assistant의 마지막 페이지에만 설정되어야 한다.
  • GTK_ASSISTANT_PAGE_PROGRESS: 작업이 완료되는 데에 오랜 시간이 소요될 경우 이는 페이지가 완료되었다고 표시될 때까지 assistant를 막을(block) 것이다. 일반 내용의 페이지와 비교 시 해당 타입의 페이지에서는 모든 버튼이 비활성화되고 사용자가 assistant를 닫을 수 없다는 데에 차이가 있다.


Gtkd caution.png 마지막 페이지 타입을 GTK_ASSISTANT_PAGE_CONFIRM, GTK_ASSISTANT_PAGE_SUMMARY 둘 중 하나로 설정하지 않을 경우 애플리케이션은 마지막 버튼 상태를 계산할 때 GTK+ 오류와 함께 취소할 것이다.


GtkAssistant는 GtkDialog로부터 파생되지 않으므로, 해당 위젯에서 gtk_dialog_run() (또는 다른 GtkDialog 함수)를 사용할 수 없다. 대신 버튼 clicked 시그널을 처리하도록 다음 네 가지 시그널이 제공된다.

  • apply: 해당 시그널은 어떤 assistant 페이지에 위치하든 Apply 버튼이나 Forward 버튼을 클릭 시 방출된다.
  • cancel: 해당 시그널은 어떤 assistant 페이지에 위치하든 Cancel 버튼을 클릭 시 방출된다.
  • close: 해당 시그널은 assistant의 마지막 페이지에서 Close 버튼이나 Apply 버튼을 클릭 시 방출된다.
  • prepare: 새 페이지를 visible로 만들기 전에 해당 시그널이 방출되면 사용자에게 표시되기 전에 개발자가 어떤 준비 작업이든 실행할 수 있다.


g_signal_connect() 또는 GLib가 제공하는 시그널 연결 함수를 이용해 모든 GtkAssistant 시그널로 연결할 수 있다. prepare를 제외하고 GtkAssistant 시그널에 대한 모든 콜백 함수들은 assistant와 사용자 데이터 매개변수를 수신한다. prepare 시그널에 대한 콜백 함수 또한 현재 페이지의 자식 위젯을 수락한다.


기본적으로 모든 페이지는 미완료(incomplete)로 설정된다. 때가 되면 gtk_assistant_set_page_complete() 를 이용해 수동으로 각 페이지를 완료(complete)로 설정해야 하는데, 그렇지 않으면 GtkAssistant 는 다음 페이지로 진행할 수 없을 것이다.

void gtk_assistant_set_page_complete (GtkAssistant *assistant,
        GtkWidget *page,
        gboolean complete);


모든 페이지에는 몇 개의 버튼과 함께 Cancel 버튼이 표시된다. 첫 페이지를 제외한 페이지에는 항상 sensitive로 설정된 Back 버튼이 표시된다. 이는 이전에 표시한 페이지로 돌아가 내용을 변경하도록 해준다.


Gtkd note.png 사용자가 Back 버튼을 클릭해 방문한 페이지는 페이지 색인에 따라 항상 앞의 페이지가 되는 것은 아니다. 이는 이전에 표시된 페이지로서, 자신의 assistant의 페이지 흐름을 정의하는 방법에 따라 차이가 있을 수 있다.


마지막 페이지를 제외한 모든 페이지에는 Forward 버튼이 위치하여 사용자가 다음 페이지로 넘어가도록 해준다. 마지막 페이지에는 Apply 버튼이 표시되어 사용자가 변경내용을 적용하도록 해준다. 하지만 페이지가 완료로 설정될 때까지는 assistant가 Forward 또는 Apply 버튼을 insensitive로 설정할 것이다. 따라서 사용자가 어떤 액션을 취할 때까지는 다음 단계로 진행하지 못할 것이다.


리스팅 5-11에서 assistant의 첫 페이지와 마지막 페이지는 완료로 설정되었는데, 정보를 제공하는 페이지에 불과했기 때문이다. 소개 페이지로 시작해 확인 또는 개요 페이지로 끝나야 하므로 대부분의 assistant에 해당하겠다.


나머지 두 페이지가 흥미로워진다. 두 번째 페이지에서 우리는 GtkEntry 위젯에 텍스트가 입력될 때까지 사용자가 진행하지 못하도록 만들고자 한다. 언제 텍스트가 삽입되었는지만 확인하면 끝나는 것처럼 보일 것이다.


하지만 사용자가 텍스트를 모두 삭제하면 어떤 일이 발생할까? 이런 경우 forward 버튼이 다시 비활성화되어야 한다. 이러한 두 액션을 모두 처리하기 위해서는 GtkEditable의 changed 시그널을 이용할 수 있다. 이는 리스팅 5-11에서와 같이 변경내용마다 엔트리 내 텍스트의 현재 상태를 확인할 수 있도록 허용할 것이다.


세 번째 페이지에서는 체크 버튼이 활성화될 때만 forward 버튼을 활성화하길 원한다. 이를 위해 GtkToggleButton의 toggled 시그널을 이용해 체크 버튼의 현재 상태를 확인하고자 하였다. 이 상태를 기반으로 forward 버튼의 민감도가 설정되었다.


네 번째 페이지에는 GTK_ASSISTANT_PAGE_PROGRESS 타입이 있는데, 이는 페이지가 완료로 설정될 때까지 모든 액션을 비활성화한다. 사용자는 버튼을 클릭하도록 지시를 받고, 마침내 버튼을 클릭하면 1초마다 GtkProgressBar 위젯을 10%씩 채우는 과정이 시작된다. 진행 막대가 채워지면 페이지는 완료로 설정된다.


GtkProgressBar

GtkAssistant 예제는 GtkProgressBar라고 불리는 또 다른 새 위젯을 소개한다. 진행 막대는 진행 과정이 얼마나 완료되었는지 보여주는 간단한 방법으로, 처리 시간이 오래 소요되는 프로세스에 유용하다. 진행 막대는 프로그램이 freeze되었다고 생각하지 않도록 진행이 되고 있는 시각적 단서를 사용자에게 제공한다.


새 진행 막대는 gtk_progress_bar_new()를 이용해 생성된다. GtkProgressBar의 구현은 GTK + 2.0의 배포판에서 훨씬 단순하게 만들어졌기 때문에 API 문서를 이용 시에는 주의를 기울여야 하는데, 문서에 표시된 함수와 프로퍼티 중 다수는 중요도가 떨어지고 있기 때문이다. 아래 소개할 두 예는 GtkProgressBar 위젯을 올바로 사용하는 방법을 보여준다.


GtkProgressBar 위젯을 사용하는 방법에는 두 가지가 있다. 프로세스가 얼마나 진행되었는지 확신할 경우 gtk_progress_bar_set_fraction()을 이용해 구분된 값을 설정해야 한다. 해당 함수는 0.0과 1.0 사이의 값을 수락하는데, 1.0은 진행 막대를 100% 완료로 설정한다.

while (percent <= 100.0)
{
    gchar *message = g_strdup_printf ("%.0f%% Complete", percent);
    gtk_progress_bar_set_fraction (progress, percent / 100.0);
    gtk_progress_bar_set_text (progress, message);

    while (gtk_events_pending ())
        gtk_main_iteration ();

    g_usleep (500000);
    percent += 5.0;
}


진행 막대를 보완하는 데에 사용 가능한 텍스트도 표시하길 원할지도 모른다. 기존 예제에서는 gtk_progress_bar_set_text()를 이용해 진행 막대 위젯에 포개어 퍼센트 완료 통계를 표시하였다.


프로세스의 진행을 감지하고 싶지 않다면 펄스를 이용할 수 있다. 기존 예제에서는 gtk_progress_bar_pulse()를 이용하여, 처리된 보류(pending) 이벤트마다 진행 막대를 한 단계씩 이동하였다. 펄스 단계는 gtk_progress_bar_set_pulse_step()을 이용해 설정할 수 있다.

gtk_progress_bar_set_pulse_step (GTK_PROGRESS_BAR (bar), 0.1);
while (gtk_events_pending ())
{
    gtk_main_iteration ();
    gtk_progress_bar_pulse ();
}


펄스 단계를 0.1로 설정하면 진행 막대는 첫 10개 단계에서 스스로 채워지고 다음 10개 단계에서 비워질 것이다. 이러한 프로세스는 진행 막대의 펄싱(pulsing)을 계속하는 한 지속될 것이다.


페이지 이동 함수

조건이 올바를 경우 특정 assistant 페이지로 건너뛰길 원할 때가 있다. 가령 자신의 애플리케이션이 새 프로젝트를 생성하고 있다고 가정해보자. 선택된 언어에 따라 세 번째나 네 번째 페이지로 건너뛰길 원한다고 치자. 이런 경우, forward 이동에 대해 자신만의 GtkAssistantPageFunc 함수를 정의하길 원할 것이다.


gtk_assistant_set_forward_page_func()를 이용하면 assistant에 대한 새 페이지 이동 함수를 정의할 수 있다. 기본적으로 GTK+는 페이지를 한 번에 하나씩 순서대로 증가시킬 것이다. 새로운 forward 함수를 정의함으로써 흐름을 정의할 수 있다.

void gtk_assistant_set_forward_page_func (GtkAssistant *assistant,
        GtkAssistantPageFunc page_func,
        gpointer data,
        GDestroyNotify destroy_func);


예를 들어, assistant_forward()는 decide_next_page()가 리턴하는 조건에 따라 페이지 2에서 페이지 3 또는 4로 이동하는 단순한 GtkAssistantPageFunc 구현이다.

static gint
assistant_forward (gint current_page,
        gpointer data)
{
    gint next_page = 0;

    switch (current_page)
    {
        case 0:
            next_page = 1;
            break;
        case 1:
            next_page = (decide_next_page() ? 2 : 3);
            break;
        case 2:
        case 3:
            next_page = 4;
            break;
        default:
            next_page = -1;
    }

    return next_page;
}


Gtkd note.png 페이지 이동 함수로부터 -1을 리턴하면 사용자에게 임계 오류가 표시되고 assistant는 다른 페이지로 이동하지 않을 것이다. 임계 오류 메시지는 사용자에게 페이지 흐름이 깨졌음을 알릴 것이다.


assistant_forward() 함수에서 흐름은 가상(fictional) 함수 decide_next_page()가 리턴한 부울 값을 기반으로 한다. 어떤 경우든 마지막 페이지는 페이지 4가 될 것이다. 현재 페이지가 범위를 벗어난 경우 -1이 리턴되어 GTK+는 예외를 던진다.


이러한 GtkAssistant 예제는 매우 간단하므로 해당 위젯의 구현은 다수의 페이지로 확장할 경우 매우 복잡해진다. 해당 위젯은 대화상자, 숨겨진 탭이 있는 GtkNotebook, 몇 개의 버튼과 함께 재생성될 수 있으나 (필자도 여러 번 실행해보았다!) 과정이 훨씬 수월하다.


자신의 이해도 시험하기

이번 장의 연습문제에서는 자신만의 커스텀 대화상자를 생성할 것이다. 각 대화상자는 여러 타입의 파일 선택자 대화상자에 해당하는 구현이 될 것이다. 하지만 내장된 대화상자의 기능을 재생성하기 위해 GtkFileChooserWidget을 GtkDialog로 내포할 것이다.


연습문제 5-1. 파일 선택자 대화상자 구현하기

이번 연습문제에서는 4개의 버튼이 있는 창을 생성한다. 각 버튼을 클릭하면 4개의 GtkFileChooser 액션 중 하나를 구현하는 각기 다른 대화상자가 열릴 것이다. 미리 빌드된 GtkFileChooserDialog 대신 GtkDialog에 추가된 GtkFileChooserWidget을 사용해야 한다.

  1. 개발자의 대화상자는 GTK_FILE_CHOOSER_ACTION_SAVE 파일 선택자 대화상자를 구현할 것이다. 선택된 파일명은 화면에 출력되어야 한다.
  2. 개발자의 대화상자는 GTK_FILE_CHOOSER_ACTION_CREATE_FOLDER 파일 선택자 대화상자를 구현할 것이다. 새 폴더명은 화면에 출력되어야 한다. 새 폴더는 g_mkdir()를 이용해 수동으로 생성해야 할 것이다.
  3. 개발자의 대화상자는 GTK_FILE_CHOOSER_ACTION_OPEN 파일 선택자 대화상자를 구현할 것이다. 선택된 파일명은 화면에 출력되어야 한다.
  4. 개발자의 대화상자는 GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER 파일 선택자 대화상자를 구현할 것이다. 선택된 폴더 경로는 화면에 출력되어야 한다.


각 대화상자에서 적당한 크기로 설정해야만 사용자에게 전체 내용이 표시될 것이다. 연습문제에서 막히는 부분이 있다면 부록 F에서 해답을 확인한다.


요약

이번 장에서는 자신만의 커스텀 대화상자를 생성하는 방법을 학습하였다. 이를 위해서는 먼저 대화상자를 초기화할 필요가 있다. 그리고 나서 대화상자의 GtkVBox에 주 내용과 함께 액션 영역 버튼이 추가되어야 한다.


대화상자는 모드형 또는 비모드형으로서 생성 가능하다. gtk_dialog_run()을 이용해 생성된 모드형 대화상자는 대화상자에 대한 메인 루프를 생성함으로써 대화상자가 소멸될 때까지 사용자가 부모 창과 상호작용하지 못하도록 한다. 또한 대화상자를 부모 창 위에서 중앙 정렬하기도 한다. 비모드형 대화상자는 사용자가 애플리케이션의 다른 창과 상호작용하도록 해주고, 강제로 대화상자에 포커스를 두지 않을 것이다.


내장된 대화상자를 학습하고 나서 독자들은 GTK+가 제공하는 내장된 대화상자의 타입에 대해 배웠다.

  • 메시지 대화상자(GtkMessageDialog): 일반 메시지, 오류 메시지, 경고, 또는 단순식의 예-아니오 질문을 사용자에게 제시한다.
  • About dialog(GtkAboutDialog): 버전, 저작권, 라이센스, 저작자 등 애플리케이션에 관한 정보를 표시한다.
  • 파일 선택자 대화상자(GtkFileChooserDialog): 사용자로 하여금 파일 선택, 여러 개의 파일 선택, 파일 저장, 디렉터리 선택, 디렉터리 생성을 허용한다.
  • 색상 선택 대화상자(GtkColorSelectionDialog): 사용자가 선택적 불투명도 값과 함께 색상을 선택할 수 있도록 해준다.
  • 글꼴 선택 대화상자(GtkFontSelectionDialog): 사용자가 글꼴과 크기, 스타일 프로퍼티를 선택할 수 있도록 해준다.


이번 장의 마지막 절은 GTK+ 2.10에서 도입된 GtkAssistant라고 불리는 위젯을 보여주었다. 이는 여러 단계로 대화상자를 생성하도록 해준다. assistant는 사실상 GtkDialog 위젯 타입이 아니지만 GtkWindow 클래스에서 직접 파생됨을 기억하는 것이 중요하다. 이는 gtk_dialog_run()을 호출하는 대신 메인 루프에서 시그널로 직접 연결하여 처리해야 함을 의미한다.


이제 GTK+의 여러 중요한 측면들을 확실히 이해했을 것이다. 좀 더 고급 위젯으로 진행하기 전에 다음 장에서는 GLib에 대한 전반적인 이해를 시키고자 한다. 제 6장은 여러 GLib 데이터 타입, idle 함수, timeouts, 프로세스 띄우기(process spawning), 스레드(thread), 동적 모듈, 파일 유틸리티, 타이머를 비롯해 기타 중요한 주제들을 다룰 것이다.


Notes