메뉴를 만들기 위해 쉬운 방법과 어려운 방법 두가지가 있다. 각각 쓰일 데가 있는 것들이지만, 우리는 보통 쉬운 방법으로 menu_factory를 쓰는 쪽을 택할 것이다. "어려운" 방법이란 함수를 이용해서 직접적으로 메뉴를 만드는 것이다. 그리고 쉬운 방법은 gtk_menu_factory 함수들을 쓰는 것이다. 이것은 훨씬 간단하지만, 나름대로 장점과 단점을 가지고 있다.
수동적인 방법으로 몇 가지 wrapper 함수들을 써 가며 메뉴를 만드는 것이 유용성에 있어서 훨씬 유리함에도 불구하고, menufactory를 이용하는 방법은 훨씬 사용하기 쉽고 또 새로운 메뉴를 추가하기도 쉽다. Menufactory를 이용하게 되면, 메뉴에 이미지라든가 '/'를 쓰는 것이 불가능해진다.
교육의 바람직한 전통에 따라, 먼저 어려운 방법부터 보이겠다.:)
메뉴바와 하위메뉴(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() 함수를 호출함으로써 붙여진다는 것이다. 이 과정을 따라보자.
static gint handler(GtkWidget *widget, GdkEvent *event);
그리고 이것은 메뉴를 팝업시킬 곳을 찾기 위해 이벤트를 이용할 것이다.
gtk_signal_connect_object(GTK_OBJECT(widget), "event",
GTK_SIGNAL_FUNC (handler), GTK_OBJECT(menu));
여기서 widget
인자는 우리가 바인딩할 widget이고, handler
인자는
핸들링 함수다. 그리고 menu
인자는 gtk_menu_new()로써 만들어진 메뉴다.
예제 코드에서 보인대로, 이것은 메뉴바에 붙여져 있는 메뉴가 될 수도
있다.
이제 분명히 해두기 위해 예제를 보도록 하자.
/* 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);
}
우리는 또한 메뉴 아이템을 반응을 보이지 않게도 만들 수 있고, 표를 참조 해서 메뉴 함수들에 키보드 바인딩을 해줄 수도 있다.
이제 어려운 방법을 보였고, gtk_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 *~
지금 당장은 이 예제뿐이다. 자세한 설명과 코멘트들은 나중에 추가될 것이다.