glib, gtk 팝업 사전 만들기 5화


전편에서 UI 설계한 거를 좀 바꿔야 겠다.


GtkToolbar 를 없애고 아래처럼 단순하게~


+---------- GtkWindow ---------+

| | +- GtkScrolledWindow --+ | |

| | | +------------------+ | | |

| | | |                  | | | |

| | | |    GtkTextView   | | | |

| | | |                  | | | |

| | | +------------------+ | | |

| | +----------------------+ | |

| +--------------------------+ |

+------------------------------+


Makefile 에 아래처럼 -lgdbm_compat 이거 넣어주고


TARGET_DEPS_LIBS = `pkg-config --libs glib-2.0 gobject-2.0 gtk+-3.0` -lgdbm_compat


configure 에 아래처럼 libgdbm_compat.so.4 파일이 존재하는지 검사하는 코드를 넣을 것.

코드 자체는 GPL 에 종속되지는 않지만 최종 바이너리가 GPL 에 링킹되기 때문에 바이너리 배포하면

소스코드를 GPL 로 공개당해야 하는 거임. 주의바람!

나중에 이걸 응용할 때에는 BSD 계열의 dbm 구현체나 다른 방식이긴 하지만 sqlite 로 교체하는 것이

좋음. 내가 여기서 작성한 문서 및 소스코드는 GPL 이 아님.

원저자이기 때문에 예외둘 수 있고, GPL 코드를 사용한 것이 아니기 때문에 내 맘대로 할 수 있음.

GPL 링킹한 바이너리 배포하는 것도 아니구!! 이거는 그냥 튜토리얼 문서임^^

참고로 FreeBSD 에 있는 dbm 구현체는 BSD 라이선스임. 리눅스 나뽀 ㅋㅋ


# check required packages if ! pkg-config --exists glib-2.0; then errmsg+=" glib-2.0"; fi if ! pkg-config --exists gobject-2.0; then errmsg+=" gobject-2.0"; fi if ! pkg-config --exists gtk+-3.0; then errmsg+=" gtk+-3.0"; fi if [ ! -f /usr/lib/libgdbm_compat.so.4 ] && [ ! -f /usr/lib/x86_64-linux-gnu/libgdbm_compat.so.4 ] then errmsg+=" libgdbm_compat.so.4" fi


그래서 tiandict.c 코드는 아래처럼 됨.

아래에서 유심히 봐야할 부분은 g_signal_connect() 로 콜백함수 연결해놓는 부분하고,

gtk_container_add (GTK_CONTAINER (dict->window), sw); 이런 식으로 컨테이너 안에

넣는다는 점. dict->window 가 GtkWindow 인데, GtkContainer 을 상속받은 거거든.

gtk_window_add() 이런 함수 없음. 정상적인 OOP 언어라면 이런 식으로 하는게 맞는데,

아무래도 c 로 하다보니, GTK_CONTAINER (dict->window) 이렇게 형변환을 하구,

gtk_container_add (GTK_CONTAINER (dict->window), sw); 이렇게 sw 를 컨테이너에 넣음.

이런 점이 OOP 언어를 쓰던 사람이 보기에는 불편할 수도 있겠네.


/* -*- Mode: C; indent-tabs-mode: nil; c-basic-offset: 2; tab-width: 2 -*- */ #include <stdlib.h> #include <gmodule.h> #include <gtk/gtk.h> #include <ndbm.h> #include <fcntl.h> typedef struct _Tiandict { DBM *dbm; /* dbm 을 사용할 거임 */ GtkWidget *window; /* GtkWindow */ GtkWidget *editor; /* GtkTextView 이름에 오해를 살 수 있는데 편집 기능이 있음 */ GtkClipboard *clipboard; /* api 문서에서 free 하지 말라고 함. 이거는 Widget 아님 */ /* g_signal_connect() 는 handler_id 를 돌려줌. 그걸 저장해 놓았다가, * 어떤 조건에 따라 g_signal_handler_disconnect 를 하기 위함 */ gulong handler_id; } Tiandict; /* 간단하게 말해서 클립보드가 바뀔 때 클립보드에서 owner-change 를 발생시키는데 그 신호에 아래 * 함수를 연결함. callback 라고 하지. 자바에서는 listener 라고 하고. * 클립보드는 X서버에 PRIMARY Atom 임. 세컨더리라는 것도 있는데 우리가 필요한 건 * PRIMARY 임. owner-change 에 대해 자세히 알려면 api 문서 참고하면 되고, * xlib 문서에 나오니까... */ void on_owner_change (GtkClipboard *clipboard, GdkEvent *event, Tiandict *dict) { GdkWindow *gdk_window; GdkSeat *seat; GdkDevice *device; gchar *text; gint x; gint y; /* 마우스로 따닥 클릭하면 그게 클립보드에 저장되는데 그걸 이렇게 받아옴 */ text = gtk_clipboard_wait_for_text (clipboard); /* text 가 NULL 일 경우도 많음 */ if (text == NULL) return; /* 윈도우 타이틀 변경하는 함수, 여기서는 타이틀바를 없애버렸으니 아무 효과도 없을 듯 ㅋㅋ */ gtk_window_set_title (GTK_WINDOW (dict->window), text); /* 아래 잘 봐봐, 좌표값 얻어오는 건데.. 첨 하는 사람들은 이런거에 한참 헤메지 */ gdk_window = gtk_widget_get_window (GTK_WIDGET (dict->window)); seat = gdk_display_get_default_seat (gdk_display_get_default ()); device = gdk_seat_get_pointer (seat); if (gdk_window && device) { gdk_device_get_position (device, NULL, &x, &y); /* 이렇게 마우스 클릭 지점으로 이동하고 */ gtk_window_move (GTK_WINDOW (dict->window), x + 10, y + 15); gtk_window_set_keep_above (GTK_WINDOW (dict->window), TRUE); gtk_window_set_keep_above (GTK_WINDOW (dict->window), FALSE); } /* 아래는 db. 지금은 그냥 이렇게 해놓았고 나중에 편집해서 저장하는 GUI 를 많들어야되겠지 */ /* 터미널에서 man dbm_fetch 이렇게 치면 설명 자세히 나옴 */ datum key; datum content; key.dptr = text; key.dsize = strlen (text) + 1; content = dbm_fetch (dict->dbm, key); if (dbm_error (dict->dbm)) perror (NULL); /* GtkTextView 는 내부에 GtkTextBuffer 가 있음. 거기에 text 를 set 하면 GtkTextView 가 화면에 보여줌 */ GtkTextBuffer *buffer; buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (dict->editor)); /* 이렇게 GtkTextBuffer 에 쓰면(set) 하면 화면에 그게 나옴 */ if (content.dptr) gtk_text_buffer_set_text (buffer, content.dptr, -1); else gtk_text_buffer_set_text (buffer, "찾는 단어가 없습니다.", -1); g_free (text); } /* 이거는 윈도우 안에 마우스 포인터 들어갈 때 실행됨 */ gboolean on_enter_notify_event (GtkWidget *widget, GdkEvent *event, Tiandict *dict) { /* 사전창에 더블클릭했을 때 거기서 또 이동하면 곤란하잖어. 그걸 방지하는 역할을 하지 */ if (dict->handler_id) { g_signal_handler_disconnect (dict->clipboard, dict->handler_id); dict->handler_id = 0; } return FALSE; } /* 이거는 윈도우 밖으로 마우스 포인터 나올 때 실행됨 */ gboolean on_leave_notify_event (GtkWidget *widget, GdkEvent *event, Tiandict *dict) { /* 위에서 방지한거 원복 */ if (!dict->handler_id) dict->handler_id = g_signal_connect (dict->clipboard, "owner-change", (GCallback) on_owner_change, dict); return FALSE; } static void on_destroy (GtkWidget *widget, gpointer user_data) { gtk_main_quit (); } Tiandict *tian_dict_new () { Tiandict *dict; GtkWidget *sw; dict = g_slice_new (Tiandict); /* man dbm_open 하면 설명 자세히 나옴 */ dict->dbm = dbm_open ("dict.db", O_RDWR | O_CREAT | O_SYNC, S_IRUSR | S_IWUSR); gtk_init (NULL, NULL); /* GtkTextView */ dict->editor = gtk_text_view_new (); /* GtkScrolledWindow */ sw = gtk_scrolled_window_new (NULL, NULL); /* 이렇게 스크롤윈도우에 에디터(GtkTextView) 를 넣음, c 언어의 한계 */ gtk_container_add (GTK_CONTAINER (sw), dict->editor); /* GtkWindow */ dict->window = gtk_window_new (GTK_WINDOW_TOPLEVEL); /* 타이틀바 없애는 거 */ gtk_window_set_decorated (GTK_WINDOW (dict->window), FALSE); gtk_window_set_default_size (GTK_WINDOW (dict->window), 350, 216); /* 창 만들어지면 가운데 위치시키라구 */ gtk_window_set_position (GTK_WINDOW (dict->window), GTK_WIN_POS_CENTER); /* 이거는 api 문서를 보기 바람. 나도 잘 모름 ㅋㅋ 설명이 매우 까다로움 */ gtk_window_set_gravity (GTK_WINDOW (dict->window), GDK_GRAVITY_CENTER); gtk_window_set_title (GTK_WINDOW (dict->window), "Tiandict"); /* 이렇게 윈도우에 스크롤윈도우를 넣음 */ gtk_container_add (GTK_CONTAINER (dict->window), sw); /* GtkClipboard */ dict->clipboard = gtk_clipboard_get (GDK_SELECTION_PRIMARY); dict->handler_id = g_signal_connect (dict->clipboard, "owner-change", (GCallback) on_owner_change, dict); /* signal */ /* 윈도 [x] 누르면 on_destroy() 함수 실행 */ g_signal_connect (dict->window, "destroy", (GCallback) on_destroy, NULL); /* 윈도안에 들어갈 때, on_enter_notify_event() 함수 실행 */ g_signal_connect (dict->window, "enter-notify-event", (GCallback) on_enter_notify_event, dict); /* 윈도안에서 나올 때, on_leave_notify_event() 함수 실행 */ g_signal_connect (dict->window, "leave-notify-event", (GCallback) on_leave_notify_event, dict); gtk_widget_show_all (dict->window); return dict; } void tian_dict_free (Tiandict *dict) { dbm_close (dict->dbm); g_slice_free (Tiandict, dict); } int main (int argc, char **argv) { Tiandict *dict; dict = tian_dict_new (); gtk_main (); tian_dict_free (dict); return EXIT_SUCCESS; }


보면 알겠지만, GUI 프로그래밍이라는 게 별거 없음. 자바 swing, swt 랑 하는 방식이 똑같음.

Qt 는 GTK 랑 더 유사함. wxWidgets 라는 거는 리눅스에서는 GTK wrapper 임.

GUI 로 뭔가를 만드려면 위에 보듯, 마우스로 클릭했을 때, 그게 PRIMARY Atom 에 저장된다는 걸

알아야겠지. 그리고 이벤트가 발생할 텐데 그 이벤트에 맞게 적절한 처리를 하는게 중요하지.

다른 GUI 프로그램도 마찬가지겠지만, api 문서 열심히 찾아서 쓰는게 일이지.

어디에서 차이가 나느냐...

사용자분들이 느낄 때, 편한가 불편한가. 이런 점.

그리고 트랜스미션(transmission) 이런 걸 만든다고 할 때, GUI 툴킷보다는 소켓, 통신 이런 걸

알아야 만들 수 있는거거덩. 소켓에서도 이벤트가 발생함. GUI 툴킷에서 제공하는 이벤트 루프에

잘 조화되게 해야겠지. glib 에서는 g_socket_create_source() 이런 함수를 제공하니까,

GMainLoop 에서 조화롭게 돌아갈 수 있지. GtkMain 은 GMainLoop 를 사용했으니 GTK 와도 조화롭게

어울릴 수 있음.

사전을 만든다고 하면, GUI 프로그래밍이 문제가 아니라 결국엔 데이터가 문제지.

그리고 편의성에 대해 생각해보자구.

이 프로그램은, toolbar 를 창 상단에 위치시키면 단순성이라는 제작 의도를 벗어나고,

그러면 프로그램을 어떻게 종료해야 하나. drag 할 때, 한글 칠 때도 클립보드(PRIMARY Atom)가

간혹 변경되는지 사전창이 따라다니는데, 헉 불편해~~~

거거를 끌 수 있는 옵션이라던가 메뉴를 제공하면 좋겠지.

appindicator(패널 알림 아이콘) 이라는게 있지. 그걸 사용하면 이런 고민거리가 확 날아가버림.

데스크탑에 알림을 보내는 방법도 있는데,

g_notification_new(), g_application_send_notification() 이런 함수가 있어서

데스크탑에 알림을 보내는 방법이 있음. 저런 걸 이용해서 사용자 편의성을 개선시키면 좋음.

appindicator 라는 라이브러리와 GNotification 은 알아두면 좋음.

GUI Toolkit 자체를 만들기 어려워 그렇지,

GUI 는 배우기 쉬움. api 문서 보고 그냥 만드면 되는 거임.

터미널에서 gtk3-demo 라고 치면,

데모 프로그램 돌아가고 거의 모든 위젯을 볼 수 있음. 소스코드도 제공하고.