glib, gtk 팝업 사전 만들기 6화
기본 동작이 완성되었습니다.
전편에서 이런저런 고민을 많이 했는데, 툴바를 안 쓰고, GtkHeader 을 타이틀바로 쓰기로 했고,
GtkHeader 에 save 버튼을 넣었습니다. 그래서 실제로 사전 역할을 합니다. 진짜임!!!!!
사전 데이터가 전혀 없는 상태라 그게 문젠데, $HOME/.config/tiandict/ 여기 디렉토리에
사전 파일이 저장됩니다. "찾는 단어가 없습니다." 이렇게 나올텐데... 그거 드래그해서 지우고 거기에
단어 뜻 넣고, 타이틀바에 있는 저장 버튼 누르면 실제로 저장됩니다. 저장된 사전은 다음에 재사용할
수 있습니다. 리눅스를 사용하면 gdbm 을 사용하여 컴파일 될텐데, gdbm 은 GPL 어플이라는거
유의하시기 바랍니다. 제가 작성한 소스코드는 ndbm.h 를 include 합니다. 이는 POSIX 표준입니다.
제가 작성한 소스코드 및 문서는 GPL 아니며 일반적인 튜토리얼 문서/소스코드입니다.
그리고,
저장된 db 는 아래의 스크립트로 단어, 뜻을 출력해볼 수 있습니다. 데이터를 교환할 때 유리합니다.
https://ruby-doc.org/stdlib-2.5.3/libdoc/gdbm/rdoc/GDBM.html
require 'gdbm'
gdbm = GDBM.new("dict.db.pag") # 시스템에 따라 파일 이름이 dict.db 이렇게 될 수도 있슴
gdbm.each_pair do |key, value|
print "#{key}: #{value}\n"
end
gdbm.close
ruby 언어로 사전을 만드는 것도 가능합니다. 어떻게 보니 루비 예찬이 ㅎㅎㅎ
require 'gdbm'
gdbm = GDBM.new("fruitstore.db") # tiandict 에서 사용하려면 파일이름을 바꿔야겠죠?
gdbm["ananas"] = "3"
gdbm["banana"] = "8"
gdbm["cranberry"] = "4909"
gdbm.close
tiandict.c
---------------------------------------------------
/* -*- Mode: C; indent-tabs-mode: nil; c-basic-offset: 2; tab-width: 2 -*- */
#include
#include
#include
#include
#include
#define TIAN_WIDTH 300
#define TIAN_HEIGHT 100
typedef struct _Tiandict
{
DBM *dbm;
GtkWidget *window;
GtkTextBuffer *text_buffer; /* 요거는 GtkTextView 용도 */
GtkClipboard *clipboard;
gulong handler_id;
/* 요거는 offset 맞추기 위한 변수, GUI 프로그래밍하다보면 offset, 좌표 맞추는 일이 자주 발생함. */
int titlebar_height;
} Tiandict;
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);
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 - 25,
y + dict->titlebar_height);
gtk_window_set_keep_above (GTK_WINDOW (dict->window), TRUE);
gtk_window_set_keep_above (GTK_WINDOW (dict->window), FALSE);
}
/* 아래는 db 에서 데이터 인출해오는 코드, 매우 간단함. */
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);
if (content.dptr)
gtk_text_buffer_set_text (dict->text_buffer, content.dptr, -1);
else
gtk_text_buffer_set_text (dict->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 ();
}
/* 이 함수는 실제로 저장하는 함수임 */
static void
on_save_button_clicked (GtkButton *button,
Tiandict *dict)
{
/* 아래 변수는 포인터가 아니잖아. 함수에 포인터로 넘길 때, &iter1 이렇게 함
아래처럼 변수를 선언하면 malloc, free 안 써도 됨. 함수 내에서 한번 쓰고 버리는 함수들은
이렇게 선언하면 됨 */
GtkTextIter iter1;
GtkTextIter iter2;
gchar *text;
/* text buffer 에서 문자열 가져오는 작업임. api 문서 보면 다 나옴.
문서를 얼마나 빨리 찾냐가 기술임 ㅋㅋㅋㅋ */
gtk_text_buffer_get_start_iter (dict->text_buffer, &iter1);
gtk_text_buffer_get_end_iter (dict->text_buffer, &iter2);
text = gtk_text_buffer_get_text (dict->text_buffer, &iter1, &iter2, TRUE);
/* text 가 NULL 이면 프로그램 죽음. 그거 방지하려고 넣는 코드.
text[0] 이거는 뭐하는 거냐면
text = ""; 이렇게 하면 text[0] 여기에 0('\0', NULL) 값 들어있는거임.
문자열은 문자열인데 strlen (text) 하면 0 나오는 문자열이거든. 그거 저장할 필요 없잖아 */
if (text && text[0])
{
datum key;
datum content;
/* 마우스로 더블 클릭하면 PRIMARY Atom 에 저장된다고 했잖어. 그걸 GtkClipboard 이라는 걸로
인출해오고, 인출 후에 set_title () 했거덩. 그래서 title 로부터 key 되는 단어를 가져오는
거임. 별도로 변수 만들기 귀찮거덩. 있는거 그냥 쓰자 */
key.dptr = (char *) gtk_window_get_title (GTK_WINDOW (dict->window));
/* 문자열은 NULL 바이트까지 저장하는게 나중에 써먹기 좋아. */
key.dsize = strlen (key.dptr) + 1;
content.dptr = text;
content.dsize = strlen (content.dptr) + 1;
/* 저장하는 함수. 예외처리해야 하는데 귀찮아서 ㅋㅋㅋ */
dbm_store (dict->dbm, key, content, DBM_REPLACE);
}
/* text[0] 에 0 들어 있을 경우도 해제되는거임. */
if (text) /* if (text != NULL) 이거랑 같은 코드. 일부러 길게 칠 필요 없거덩 */
g_free (text);
}
Tiandict *tian_dict_new ()
{
Tiandict *dict;
GtkWidget *sw; /* 이거는 scrolled window, 길어서 그냥 sw */
GtkWidget *editor;
GtkWidget *titlebar; /* GtkHeader */
GtkWidget *save_button; /* GtkButton */
gchar *path1, *path2; /* 패스1은 디렉토리 만들 때 써먹을 거임 */
dict = g_slice_new (Tiandict);
path1 = g_build_path (G_DIR_SEPARATOR_S, g_get_user_config_dir (),
"tiandict", NULL);
path2 = g_build_path (G_DIR_SEPARATOR_S, g_get_user_config_dir (),
"tiandict", "dict.db", NULL);
g_mkdir_with_parents (path1, 0700); /* 디렉토리 한방에 만드는거 mkdir -p */
dict->dbm = dbm_open (path2, O_RDWR | O_CREAT | O_SYNC, S_IRUSR | S_IWUSR);
if (!dict->dbm)
{
/* glib 에 g_log, g_info, g_message, g_warning, g_critical, g_debug
이런 함수들 있음. 참고로 logging 도 됨 */
g_critical ("Can't open %s", path2);
exit (EXIT_FAILURE);
}
gtk_init (NULL, NULL);
/* GtkButton */
/* button 은 이렇게 만드는데, icon 이름에,
/usr/share/icons/Adwaita/22x22/legacy/document-save.png
document-save 요 부분만 넣으면 됨. 각종 사이즈, 각종 theme 별로,
document-save 아이콘이 엄청 나게 많음.
/usr/share/icons/ 디렉토리에 있는 거니까.. 고민하지 말라구. */
save_button = gtk_button_new_from_icon_name ("document-save",
GTK_ICON_SIZE_SMALL_TOOLBAR);
/* 전편에 설명했지? 버튼 클릭하면 clicked 신호가 발생하는데,
on_save_button_clicked 함수 실행하라는 얘기임. user_data 로 dict 넘어가는거구 */
g_signal_connect (save_button, "clicked",
G_CALLBACK (on_save_button_clicked), dict);
/* GtkHeaderBar */
titlebar = gtk_header_bar_new ();
gtk_header_bar_set_decoration_layout (GTK_HEADER_BAR (titlebar),
"menu:minimize,close");
gtk_header_bar_set_show_close_button (GTK_HEADER_BAR (titlebar), TRUE);
/* gtk_container_add (GTK_HEADER_BAR (titlebar), aoeuao) 이런 함수도 있지만,
아래처럼 직접 제공하면 그걸 써!!.
pack_start, pack_end 함수 많이 보게 될 텐데,
pack_start 는 시작부분(왼쪽이겠지?)부터 패킹하라는 얘기. 쳐넣으라는 얘기.
pack_end 는 끝에서부터 (오른쪽이겠지?) 패킹하라는 얘기. 쳐넣으라는 얘기.
이런 짓을 왜하지? 이러지 말구, 창 최대화하면 그게 layout 에 영향을 주거덩.
왜 필요한지 사이즈, 간격 맞추다보면 별 뻘짓을 다하게 될꺼다 흠핫핫핫 ㅋㅋ */
gtk_header_bar_pack_start (GTK_HEADER_BAR (titlebar), save_button);
/* GtkTextView */
editor = gtk_text_view_new ();
/* 이거는 줄바꿈, 편집기에 이렇거 많이 보이지? */
gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (editor), GTK_WRAP_WORD_CHAR);
/* 이거는 버퍼 얻어오는거. 나중에 해제 시키지 말구 */
dict->text_buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (editor));
gtk_text_buffer_set_text (dict->text_buffer,
"티엔 사전입니다. 마우스로 더블 클릭하면 단어에 대한 뜻이 나옵니다. 찾는 단어가 없을 때, "
"이 곳에 단어에 대한 의미를 입력후에 위에 제목 줄에 저장 버튼 누르시면 되겠습니다.", -1);
/* GtkScrolledWindow */
/* 옵션 안 주면 스크롤바가 알아서 다 자동으로 됨 */
/* 스코롤러 정책 옵션 바꿀려면 devhelp 로 gtk_scrolled 입력하고 검색하면 되구.
GUI 프로그래밍은 api 검색 능력이 곧 실력이야. 진짜임!!! */
sw = gtk_scrolled_window_new (NULL, NULL);
gtk_container_add (GTK_CONTAINER (sw), editor);
/* GtkWindow */
dict->window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
gtk_window_set_titlebar (GTK_WINDOW (dict->window), titlebar);
gtk_window_set_default_size (GTK_WINDOW (dict->window), TIAN_WIDTH, TIAN_HEIGHT);
gtk_window_set_position (GTK_WINDOW (dict->window), GTK_WIN_POS_CENTER);
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);
g_signal_connect (dict->window, "destroy", (GCallback) on_destroy, NULL);
g_signal_connect (sw, "enter-notify-event", (GCallback) on_enter_notify_event, dict);
g_signal_connect (sw, "leave-notify-event", (GCallback) on_leave_notify_event, dict);
gtk_widget_show_all (dict->window);
/* 요거는 진짜 설명이 필요함. 위에 gtk_header_bar_new() 했잖아. 그때는 height 를
알 수 없어. 그걸 나중에 allocate 한 후에 알 수 있거든.
gtk_widget_show_all gtk_widget_show 한 이후에는 확실히 알 수 있지.
그거 모르면 엄청 해멜거다.. ㅋㅋㅋ "draw" 신호가 발생하거든.
그 신호에 g_signal_connect 로 연결하고, 콜백에서 아래 함수 실행해도 크기 알 수 있지
이거 진짜 노하우야... 잘 기억해둬. 나 옛날에 이거 몰라서 한 두달 삽질했다 !!!!
그 옛날이 벌써 4년전이네 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ
*/
dict->titlebar_height = gtk_widget_get_allocated_height (titlebar);
g_free (path1);
g_free (path2);
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;
}
configure
------------------------------
#!/bin/sh
# default variables
prefix="/usr/local"
EXTRA_CFLAGS="-Wall -Werror"
EXTRA_LDFLAGS="-Wl,-z,relro"
hardening="-D_FORTIFY_SOURCE=2 -O1"
for i in "$@"
do
case $i in
--disable-hardening)
hardening=""
;;
--libdir=*)
libdir="${i#*=}"
;;
--prefix=*)
prefix="${i#*=}"
;;
--help|*)
echo "Usage: ./configure [OPTION]..."
echo " --help Show help messages"
echo " --disable-hardening Disable hardening"
echo " --libdir=PATH Set PATH to libdir"
echo " --prefix=DIRECTORY Set DIRECTORY to prefix"
exit 1
;;
esac
done
[ -z $libdir ] && libdir="$prefix/lib"
eval libdir=$libdir
EXTRA_CFLAGS=$EXTRA_CFLAGS" $hardening"
errmsg=""
# 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
# print error messages
if test -n "$errmsg"; then
echo "Not found: $errmsg"
exit 1
fi
# generate config.mk
cat > config.mk <<- EOF
EXTRA_CFLAGS = $EXTRA_CFLAGS
EXTRA_LDFLAGS = $EXTRA_LDFLAGS
VERSION = 2020.03.07
CC = cc
top_srcdir = $(pwd)
prefix = $prefix
libdir = $libdir
datadir = $prefix/share
GETTEXT_PACKAGE = tiandict
TARGET_LOCALE_DIR = $prefix/share/locale
MKDIR_P = mkdir -p
EOF
echo "Generated config.mk"
echo "----------------------------------------------------------------"
cat config.mk
echo "----------------------------------------------------------------"
Makefile
-------------------------
include ./config.mk
TARGET = tiandict
TARGET_DEPS_CFLAGS = `pkg-config --cflags glib-2.0 gobject-2.0 gtk+-3.0`
TARGET_DEPS_LIBS = `pkg-config --libs glib-2.0 gobject-2.0 gtk+-3.0` -lgdbm_compat
SOURCES = tiandict.c
CFLAGS = \
$(TARGET_DEPS_CFLAGS) \
$(EXTRA_CFLAGS) \
-DVERSION=\"$(VERSION)\" \
-DTARGET_LOCALE_DIR=\"$(TARGET_LOCALE_DIR)\"
LDFLAGS = $(TARGET_DEPS_LIBS) $(EXTRA_LDFLAGS)
all: $(TARGET)
$(TARGET): $(SOURCES) Makefile
$(CC) $(CFLAGS) $(SOURCES) \
-Wl,--as-needed $(LDFLAGS) -o $(TARGET)
clean:
rm -f $(TARGET) config.mk
install:
install -D -m 755 $(TARGET) -t $(DESTDIR)$(prefix)/bin
uninstall:
rm -f $(DESTDIR)$(prefix)/bin/$(TARGET)
-rmdir -p $(DESTDIR)$(prefix)/bin
그리고 g_signal_connect_object() 함수 알아두면 좋구.
g_signal_connect() 함수로 마지막 인수가 콜백함수에 user_data 로 넘어가잖아.
마지막 인수가 해제되었는데 콜백함수가 실행되면 프로그램 죽겠지?
그거 방지할 수 있는 함수가 g_signal_connect_object() 함수임. 마지막 인수가
GObect 상속받은 것이어야 함. GtkTextView 이런거 GObject 임.
위에서는 g_signal_connect_object() 이 함수를 쓰지 않아도 되니까 안 썼는데,
만약 플러그인(모듈, so 파일) 다룬다면 g_signal_connect_object() 이 함수 써야할 일이
생길거임. 콜백으로 넘긴 인수가 먼저 메모리 해제되는거 고려하지 않으면 디버깅하는데
엄청 애먹지. 위에서는 그럴 일 없구.
실제로 200줄 정도 밖에 안 되는 코드야..
200줄로 이렇게 실용적인 걸 만들 수 있음!!!!!
그러니까 꼭 배워놓도록 해!!
이거 하루 이틀이면 만드는 건데 설명하냐고 며칠 걸린 거구^^
나중에 appindicator, GNotification 사용하면 편의성이 더욱 개선될 거구.
레포지토리 만드는 방법은 전에 이미 설명한 바 있구.
https://gall.dcinside.com/mgallery/board/view/?id=github&no=6433&s_type=search_all&s_keyword=pacman&page=1
https://gall.dcinside.com/mgallery/board/view/?id=github&no=6463&s_type=search_all&s_keyword=%EB%A7%8C%EC%9E%90%EB%A1%9C&page=1
기획, 설계, 개발, 패키지 배포까지 원스톱으로 할 줄 알아야겠지?
그래야 개발자지!! 어찌 좀 발음이 ㅋㅋㅋ
Makefile 에서 all: install: uninstall: clean: 이런게 있는데 그 다음 줄은 TAB 으로 시작하는 거임.
실제로 작동하는 파일이니까... 개인 사전 필요하면 컴파일해서 쓰구.
사전 만들기 연재는 여기서 끝^^
어찌... 파일이 중간 중간 짤려버렸냐. 7화에 파일만 다시 올림