다음 이전 차례

11. 메뉴 widget

메뉴를 만들기 위해 쉬운 방법과 어려운 방법 두가지가 있다. 각각 쓰일 데가 있는 것들이지만, 우리는 보통 쉬운 방법으로 menu_factory를 쓰는 쪽을 택할 것이다. "어려운" 방법이란 함수를 이용해서 직접적으로 메뉴를 만드는 것이다. 그리고 쉬운 방법은 gtk_menu_factory 함수들을 쓰는 것이다. 이것은 훨씬 간단하지만, 나름대로 장점과 단점을 가지고 있다.

수동적인 방법으로 몇 가지 wrapper 함수들을 써 가며 메뉴를 만드는 것이 유용성에 있어서 훨씬 유리함에도 불구하고, menufactory를 이용하는 방법은 훨씬 사용하기 쉽고 또 새로운 메뉴를 추가하기도 쉽다. Menufactory를 이용하게 되면, 메뉴에 이미지라든가 '/'를 쓰는 것이 불가능해진다.

11.1 수동적으로 메뉴 만들기

교육의 바람직한 전통에 따라, 먼저 어려운 방법부터 보이겠다.:)

메뉴바와 하위메뉴(submenu)들를 만드는데 쓰는 세가지 widget이 있다.

메뉴 아이템 widget이 두가지 다른 용도로 쓰일 수 있다는 점때문에 약간 복잡한 면이 있다. 메뉴 아이템은 단순히 메뉴 위에 놓일 수도 있고 또는 메뉴바 위에 놓여서 선택되었을 때 특정 메뉴를 활성화시키도록 쓰일 수도 있다.

메뉴와 메뉴바를 만들기 위해 쓰이는 함수들을 살펴보자. 이 첫번째 함수는 새로운 메뉴바를 만들기 위해 쓰인다.

GtkWidget *gtk_menu_bar_new()

이것은 이름 그대로 새로운 메뉴바를 만든다. 버튼과 마찬가지로, 우리는 이것을 윈도에 패킹하기 위해 gtk_container_add를 이용할수도 있고, 또는 박스에 패킹하기 위해 box_pack 함수들을 이용할 수 있다. - 버튼과 같다.

GtkWidget *gtk_menu_new();

이 함수는 새로운 메뉴를 향한 포인터를 리턴하는데, 이것은 실제로 보여지지는 않고(gtk_widget_show를 통해) 다만 메뉴 아이템들을 가지고만 있다. 이 아래에 나오는 예제를 보며 더 명확히 이해하기를 바란다.

이번의 두 함수는 메뉴나 메뉴바 안으로 패킹되는 메뉴 아이템을 만들기 위해 쓰인다.

GtkWidget *gtk_menu_item_new()

GtkWidget *gtk_menu_item_new_with_label(const char *label)

이 함수들은 보여지기 위한 메뉴를 만들 때 쓰인다. gtk_menu_new로써 만들어지는 "메뉴"와 gtk_menu_item_new로써 만들어지는 "메뉴 아이템"을 꼭 구별해야 한다. 메뉴 아이템은 연결된 동작이 있는 실제의 버튼이 될 것이지만, 반면 메뉴는 이것들을 가지고 있는 컨테이너가 될 것이다.

gtk_menu_new_with_label과 단순한 gtk_menu_new 함수는 여러분이 버튼에 대해 공부한 후에 짐작하는 그대로다. gtk_menu_new_with_label은 라벨이 이미 패킹되어 있는 메뉴 아이템을 만들고, gtk_menu_new는 비어있는 메뉴 아이템을 만든다.

한번 메뉴 아이템을 만들면 반드시 이를 메뉴 안에 넣어야만 한다. 이는 gtk_menu_append 함수를 이용해서 이루어진다. 어떤 아이템이 사용자에 의해 선택되었을 때 이를 알아내어 처리하기 위해서는 activate 시그널을 통상적으로 하듯이 연결한다. 그래서 만일 Open, Save, Quit 옵션을 가진 표준 File 메뉴를 만들고자 한다면 소스 코드는 다음과 같이 된다.

file_menu = gtk_menu_new();    /* 메뉴를 보여줄 필요는 없다. */

/* 메뉴 아이템들을 만든다. */
open_item = gtk_menu_item_new_with_label("Open");
save_item = gtk_menu_item_new_with_label("Save");
quit_item = gtk_menu_item_new_with_label("Quit");

/* 그것들을 메뉴에 붙인다. */
gtk_menu_append( GTK_MENU(file_menu), open_item);
gtk_menu_append( GTK_MENU(file_menu), save_item);
gtk_menu_append( GTK_MENU(file_menu), quit_item);

/* "activate" 시그널과 callback 함수를 연결한다. */
gtk_signal_connect_object( GTK_OBJECT(open_items), "activate",
                   GTK_SIGNAL_FUNC(menuitem_response), (gpointer) "file.open");
gtk_signal_connect_object( GTK_OBJECT(save_items), "activate",
                   GTK_SIGNAL_FUNC(menuitem_response), (gpointer) "file.save");

/* Quit 메뉴 아이템에  exit 함수를 연결한다. */
gtk_signal_connect_object( GTK_OBJECT(quit_items), "activate",
                   GTK_SIGNAL_FUNC(destroy), (gpointer) "file.quit");

/* 이제 메뉴 아이템들을 보여줘야 한다. */
gtk_widget_show( open_item );
gtk_widget_show( save_item );
gtk_widget_show( quit_item );

여기까지하면 필요한 메뉴는 일단 만든 것이다. 이제 지금까지 만든 메뉴를 붙일 File 메뉴 아이템과 메뉴바를 만들어야 한다. 코드는 이렇게 된다.

menu_bar = gtk_menu_bar_new();
gtk_container_add( GTK_CONTAINER(window), menu_bar);
gtk_widget_show( menu_bar );

file_item = gtk_menu_item_new_with_label("File");
gtk_widget_show(file_item);

이제 file_item을 메뉴와 연결해야 한다. 이것은 다음 함수를 통해 이루어진다.

void gtk_menu_item_set_submenu( GtkMenuItem *menu_item, GtkWidget *submenu);

그래서 우리 예제는 다음 코드로 이어진다.

gtk_menu_item_set_submenu( GTK_MENU_ITEM(file_item), file_menu);

해야할 남은 모든 일은 메뉴를 메뉴바에 붙이는 일이다. 이는 다음 함수를 이용한다.

void gtk_menu_bar_append( GtkMenuBar *menu_bar, GtkWidget *menu_item);

우리 코드에서는 다음과 같이 된다.

gtk_menu_bar_append( GTK_MENU_BAR (menu_bar), file_item );

만일 메뉴들이 help 메뉴가 자주 그러는 것처럼 메뉴바의 오른쪽에 위치하게 하고 싶다면 메뉴바에 메뉴를 붙이기 전에 다음 함수를 쓴다. (현재 예제라면 인자로 file_item을 주면 된다.)

void gtk_menu_item_right_justify (GtkMenuItem *menu_item);

다음은 메뉴들이 달려있는 메뉴바를 만드는 단계들에 대한 요약이다.

팝업메뉴를 만드는 것도 거의 같다. 다른 점이 있다면 메뉴는 메뉴바에 의해 자동적으로 붙여지는 것이 아니라, button_press 이벤트로부터 gtk_menu_popup() 함수를 호출함으로써 붙여진다는 것이다. 이 과정을 따라보자.

11.2 수동으로 메뉴를 만드는 예제

이제 분명히 해두기 위해 예제를 보도록 하자.

/* menu.c */

#include <gtk/gtk.h>

static gint button_press (GtkWidget *, GdkEvent *);
static void menuitem_response (GtkWidget *, gchar *);

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

    GtkWidget *window;
    GtkWidget *menu;
    GtkWidget *menu_bar;
    GtkWidget *root_menu;
    GtkWidget *menu_items;
    GtkWidget *vbox;
    GtkWidget *button;
    char buf[128];
    int i;

    gtk_init (&argc, &argv);

    /* 윈도를 만든다. */
    window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_widget_set_usize( GTK_WIDGET (window), 200, 100);
    gtk_window_set_title(GTK_WINDOW (window), "GTK Menu Test");
    gtk_signal_connect(GTK_OBJECT (window), "delete_event",
                       (GtkSignalFunc) gtk_main_quit, NULL);

    /* menu-widget을 시작한다.  여기서 메뉴 widget들에 대해
     * gtk_show_widget()을 쓰면 안된다!
     * 이것은 메뉴 아이템을 가지고 있는 메뉴고, 우리가 어플에서
     * "Root Menu"를 클릭했을 때 팝업될 것이다. */
    menu = gtk_menu_new();

    /* 다음으로 우리는 세 메뉴엔트리를 만들기 위해 작은 루프를
     * 구현한다.  보통이라면, 우리는 각 메뉴 아이템들에 대해
     * "clicked" 시그널을 잡아낼 것이고, 그것을 위해 callback을
     * 세팅할 것이다.  그러나 공간을 절약하기 위해 그 과정은
     * 생략한다. */

    for(i = 0; i < 3; i++)
        {
            /* buf로 메뉴 이름을 복사한다. */
            sprintf(buf, "Test-undermenu - %d", i);

            /* 이름을 가진 새 메뉴 아이템을 만든다. */
            menu_items = gtk_menu_item_new_with_label(buf);

            /* 이것을 메뉴에 첨가한다. */
            gtk_menu_append(GTK_MENU (menu), menu_items);

            /* 메뉴 아이템이 선택되면 뭔가 쓸만한 동작을 시킨다. */
            gtk_signal_connect (GTK_OBJECT(menu_items), "activate",
                GTK_SIGNAL_FUNC(menuitem_response), (gpointer)
                g_strdup(buf));

            /* widget을 보인다. */
            gtk_widget_show(menu_items);
        }

    /* 이것은 root 메뉴며, 메뉴바에 나타날 메뉴의 이름 즉 라벨이
     * 될 것이다.  이것은 단지 눌러졌을 때 메뉴의 나머지 부분이
     * 팝업되기만 할 것이므로 특별히 시그널 핸들러가 결합되어
     * 있을 필요는 없다. */
    root_menu = gtk_menu_item_new_with_label("Root Menu");

    gtk_widget_show(root_menu);

      /* 이제 우리의 새롭게 만들어진 "menu"를, "root menu"가 되도록
       * 설정해 보자. */
      gtk_menu_item_set_submenu(GTK_MENU_ITEM (root_menu), menu);

      /* 메뉴와 버튼을 들여놓을 vbox */
      vbox = gtk_vbox_new(FALSE, 0);
      gtk_container_add(GTK_CONTAINER(window), vbox);
      gtk_widget_show(vbox);

      /* 메뉴를 담고 있을 menu-bar를 만들고 그것을 우리의 main 윈도에
       * 추가한다. */
      menu_bar = gtk_menu_bar_new();
      gtk_box_pack_start(GTK_BOX(vbox), menu_bar, FALSE, FALSE, 2);
      gtk_widget_show(menu_bar);

      /* 메뉴를 팝업시키도록 연결될 한 버튼을 만든다. */
      button = gtk_button_new_with_label("press me");
      gtk_signal_connect_object(GTK_OBJECT(button), "event",
          GTK_SIGNAL_FUNC (button_press), GTK_OBJECT(menu));
      gtk_box_pack_end(GTK_BOX(vbox), button, TRUE, TRUE, 2);
      gtk_widget_show(button);

      /* 끝으로 menu-item을 menu-bar에 이어준다.  이것이 바로
       * 내가 지금껏 지껄여 온 "root" 메뉴 아이템이다. =) */
      gtk_menu_bar_append(GTK_MENU_BAR (menu_bar), root_menu);

      /* 언제나 전체 윈도를 마지막에 보여준다. */
      gtk_widget_show(window);

      gtk_main ();

    return 0;
}

/* "widget"으로 넘겨받은 메뉴를 보임으로써 button-press에 응답한다.
 *
 * 인자 "widget"은 눌려진 버튼이 아니라 보여질 메뉴라는 걸 기억하자. */

static gint button_press (GtkWidget *widget, GdkEvent *event)
{

      if (event->type == GDK_BUTTON_PRESS) {
          GdkEventButton *bevent = (GdkEventButton *) event;
          gtk_menu_popup (GTK_MENU(widget), NULL, NULL, NULL, NULL,
                          bevent->button, bevent->time);
          /* 우리가 이 이벤트를 다루었음을 말한다.  여기서 멈춘다. */
          return TRUE;
      }

      /* 우리가 이 이벤트를 다루지 않았음을 말한다.  계속 지나친다. */
      return FALSE;
}

/* 메뉴 아이템이 선택되었을 때 문자열을 프린트한다. */

static void menuitem_response (GtkWidget *widget, gchar *string)
{
    printf("%s\n", string);
}

우리는 또한 메뉴 아이템을 반응을 보이지 않게도 만들 수 있고, 표를 참조 해서 메뉴 함수들에 키보드 바인딩을 해줄 수도 있다.

11.3 GtkMenuFactory를 이용하기

이제 어려운 방법을 보였고, gtk_menu_factory 함수들을 이용하는 방법이 여기 있다.

11.4 Menu factory의 예제

이것은 GTK menu factory를 이용하는 예제이다. 이것은 첫번째 파일 menus.h 다. 우리는 menus.c에서 쓰인 global변수들을 고려해서 menus.c 와 main.c를 분리할 것이다.

/* menufactory.h */

#ifndef __MENUFACTORY_H__
#define __MENUFACTORY_H__

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

void get_main_menu (GtkWidget **menubar, GtkAcceleratorTable **table);
void menus_create(GtkMenuEntry *entries, int nmenu_entries);

#ifdef __cplusplus
}
#endif /* __cplusplus */

#endif /* __MENUFACTORY_H__ */

그리고 이것은 menufactory.c 파일이다.

/* menufactory.c */

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

#include "mfmain.h"

static void menus_remove_accel(GtkWidget * widget, gchar * signal_name, gchar * path);
static gint menus_install_accel(GtkWidget * widget, gchar * signal_name, gchar key, gchar modifiers, gchar * path);
void menus_init(void);
void menus_create(GtkMenuEntry * entries, int nmenu_entries);

/* 이것은 새로운 메뉴를 만들기 위해 쓴 GtkMenuEntry 구조체다.
 * 첫번째 멤버는 메뉴를 정의하는 문자열이다.  두번째는 이 메뉴함수를
 * 키보드로 불러낼 때 쓰이는 디폴트 단축키다.  세번째 멤버는 이 메뉴
 * 아이템이 선택되었을(마우스 혹은 단축키로써) 때 호출될 callback 함수다.
 * 그리고 마지막 멤버는 이 callback함수에 넘겨질 데이터다. */

static GtkMenuEntry menu_items[] =
{
    {"<Main>/File/New", "<control>N", NULL, NULL},
    {"<Main>/File/Open", "<control>O", NULL, NULL},
    {"<Main>/File/Save", "<control>S", NULL, NULL},
    {"<Main>/File/Save as", NULL, NULL, NULL},
    {"<Main>/File/<separator>", NULL, NULL, NULL},
    {"<Main>/File/Quit", "<control>Q", file_quit_cmd_callback, "OK, I'll quit"},
    {"<Main>/Options/Test", NULL, NULL, NULL}
};

/* 메뉴 아이템의 갯수를 계산한다. */
static int nmenu_items = sizeof(menu_items) / sizeof(menu_items[0]);

static int initialize = TRUE;
static GtkMenuFactory *factory = NULL;
static GtkMenuFactory *subfactory[1];
static GHashTable *entry_ht = NULL;

void get_main_menu(GtkWidget ** menubar, GtkAcceleratorTable ** table)
{
    if (initialize)
            menus_init();

    if (menubar)
            *menubar = subfactory[0]->widget;
    if (table)
            *table = subfactory[0]->table;
}

void menus_init(void)
{
    if (initialize) {
        initialize = FALSE;

        factory = gtk_menu_factory_new(GTK_MENU_FACTORY_MENU_BAR);
        subfactory[0] = gtk_menu_factory_new(GTK_MENU_FACTORY_MENU_BAR);

        gtk_menu_factory_add_subfactory(factory, subfactory[0], "<Main>");
        menus_create(menu_items, nmenu_items);
    }
}

void menus_create(GtkMenuEntry * entries, int nmenu_entries)
{
    char *accelerator;
    int i;

    if (initialize)
            menus_init();

    if (entry_ht)
            for (i = 0; i < nmenu_entries; i++) {
                accelerator = g_hash_table_lookup(entry_ht, entries[i].path);
                if (accelerator) {
                    if (accelerator[0] == '\0')
                            entries[i].accelerator = NULL;
                    else
                            entries[i].accelerator = accelerator;
                }
            }
    gtk_menu_factory_add_entries(factory, entries, nmenu_entries);

    for (i = 0; i < nmenu_entries; i++)
            if (entries[i].widget) {
                gtk_signal_connect(GTK_OBJECT(entries[i].widget), "install_accelerator",
                         (GtkSignalFunc) menus_install_accel,
                         entries[i].path);
                gtk_signal_connect(GTK_OBJECT(entries[i].widget), "remove_accelerator",
                         (GtkSignalFunc) menus_remove_accel,
                         entries[i].path);
            }
}

static gint menus_install_accel(GtkWidget * widget, gchar * signal_name, gchar key, gchar modifiers, gchar * path)
{
    char accel[64];
    char *t1, t2[2];

    accel[0] = '\0';
    if (modifiers & GDK_CONTROL_MASK)
            strcat(accel, "<control>");
    if (modifiers & GDK_SHIFT_MASK)
            strcat(accel, "<shift>");
    if (modifiers & GDK_MOD1_MASK)
            strcat(accel, "<alt>");

    t2[0] = key;
    t2[1] = '\0';
    strcat(accel, t2);

    if (entry_ht) {
        t1 = g_hash_table_lookup(entry_ht, path);
        g_free(t1);
    } else
            entry_ht = g_hash_table_new(g_string_hash, g_string_equal);

    g_hash_table_insert(entry_ht, path, g_strdup(accel));

    return TRUE;
}

static void menus_remove_accel(GtkWidget * widget, gchar * signal_name, gchar * path)
{
    char *t;

    if (entry_ht) {
        t = g_hash_table_lookup(entry_ht, path);
        g_free(t);

        g_hash_table_insert(entry_ht, path, g_strdup(""));
    }
}

void menus_set_sensitive(char *path, int sensitive)
{
    GtkMenuPath *menu_path;

    if (initialize)
            menus_init();

    menu_path = gtk_menu_factory_find(factory, path);
    if (menu_path)
            gtk_widget_set_sensitive(menu_path->widget, sensitive);
    else
            g_warning("Unable to set sensitivity for menu which doesn't exist: %s", path);
}

이것은 mfmain.h다.

/* mfmain.h */

#ifndef __MFMAIN_H__
#define __MFMAIN_H__

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

void file_quit_cmd_callback(GtkWidget *widget, gpointer data);

#ifdef __cplusplus
}
#endif /* __cplusplus */

#endif /* __MFMAIN_H__ */

그리고 이것이 mfmain.c다.

/* mfmain.c */

#include <gtk/gtk.h>

#include "mfmain.h"
#include "menufactory.h"

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

    GtkAcceleratorTable *accel;

    gtk_init(&argc, &argv);

    window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_signal_connect(GTK_OBJECT(window), "destroy",
                       GTK_SIGNAL_FUNC(file_quit_cmd_callback),
                       "WM destroy");
    gtk_window_set_title(GTK_WINDOW(window), "Menu Factory");
    gtk_widget_set_usize(GTK_WIDGET(window), 300, 200);

    main_vbox = gtk_vbox_new(FALSE, 1);
    gtk_container_border_width(GTK_CONTAINER(main_vbox), 1);
    gtk_container_add(GTK_CONTAINER(window), main_vbox);
    gtk_widget_show(main_vbox);

    get_main_menu(&menubar, &accel);
    gtk_window_add_accelerator_table(GTK_WINDOW(window), accel);
    gtk_box_pack_start(GTK_BOX(main_vbox), menubar, FALSE, TRUE, 0);
    gtk_widget_show(menubar);

    gtk_widget_show(window);
    gtk_main();

    return(0);
}

/* 여기서는 menufactory를 이용할 때 callback들이 어떻게 작동하는지를
 * 보여준다.  종종, 사람들은 메뉴들로부터의 모든 callback을 별도의 파일에
 * 모아두고, 거기서 적절한 함수를 호출해 쓰는 방법을 택하기도 한다. */
void file_quit_cmd_callback (GtkWidget *widget, gpointer data)
{
    g_print ("%s\n", (char *) data);
    gtk_exit(0);
}

그리고 컴파일을 쉽게 해주는 makefile이다.

# Makefile.mf

CC      = gcc
PROF    = -g
C_FLAGS =  -Wall $(PROF) -L/usr/local/include -DDEBUG
L_FLAGS =  $(PROF) -L/usr/X11R6/lib -L/usr/local/lib
L_POSTFLAGS = -lgtk -lgdk -lglib -lXext -lX11 -lm
PROGNAME = menufactory

O_FILES = menufactory.o mfmain.o

$(PROGNAME): $(O_FILES)
    rm -f $(PROGNAME)
    $(CC) $(L_FLAGS) -o $(PROGNAME) $(O_FILES) $(L_POSTFLAGS)

.c.o:
    $(CC) -c $(C_FLAGS) $<

clean:
    rm -f core *.o $(PROGNAME) nohup.out
distclean: clean
    rm -f *~

지금 당장은 이 예제뿐이다. 자세한 설명과 코멘트들은 나중에 추가될 것이다.


다음 이전 차례