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 으로 시작하는 거임.

실제로 작동하는 파일이니까... 개인 사전 필요하면 컴파일해서 쓰구.

사전 만들기 연재는 여기서 끝^^