Wprowadzenie do Gtk+2

Zbigniew Jurkiewicz, IIUW

Gtk+ jest zbiorem bibliotek do pisanie interfejsu GUI, informacje o nim znajdują się na tej stronie. W poniższym opisie ograniczymy się tylko do podstawowych informacji. Dokumentację do Gtk+2 można znaleźć pod tym adresem, a w większości dystrybucji Linuxa w menu ,Programowanie' albo ,Narzędzia dla programistów' jest Devhelp.

W sieci są różne tutoriale do Gtk+, np. ZetCode.

Programy przykładowe opisywane w tekście znajdują się tu

1. GTK: rozpoznanie bojem

GTK+ to zbiór bibliotek w C do programowania interfejsów ,,graficznych'', objemujących takie rzeczy jak menu, przewijane okna, dialogi z polami. Został napisany podczas implementacji programu GIMP. Pomimo że jest napisany w C posiada własne mechanizmy obiektowości, z klasami i dziedziczeniem, ale spróbujemy trzymać się od nich możliwie z daleka. Ma też półautomatyczne odśmiecanie, oparte na licznikach odwołań.

Trzeba pamiętać, że Gtk został napisany nie tylko jako biblioteka do C, ale również jako mechanizm do dostarczania swoich usług językom wyższego poziomu (tzw. bindings), stąd takie a nie inne rozwiązania w pewnych sytuajach.

Podstawowym pojęciem w GTK jest widget (zwany czasem ,,kontrolką''), przykłady to przyciski, suwaki czy pola tekstowe. Niektóre z widgetów to kontenery mogące zawierać w środku inne widgety, najpopularniejszy kontener to okno.

Programy w GTK są sterowane zdarzeniami: po wyświetleniu wszystkich obiektów na ekranie program wchodzi w główną pętlę czekającą na zdarzenia i obsługującą je. Zdarzeniem może być na przykład przyciśnięcie przycisku myszy na jakimś widocznym obiekcie. Zdarzenie powoduje wysłanie odpowiedniego nazwanego sygnału do obiektów, które zadeklarowały ich obsługę.

Zdarzenia pochodzą od substratu, na którym jest osadzone GTK, na przykład of X-Windows. Gtk przekształca je na sygnały, których obsługę możemy określać. Sygnały mogą bezpośrednio odpowiadać zdarzeniom, na przykład "button-released", jak też być generowane pośrednio w wyniku spełnienia pewnych warunków, na przykład sygnał "clicked" dla przycisku.

Struktura niedużego programu w GTK jest prosta. Na początku powinniśmy umieścić wiersz

#include <gtk/gtk.h>
Spowoduje to dołączenie pozostałych potrzebnych plików nagłówkowych.

Następnie definiujemy potrzebne procedury, a w programie głównym:

Pełny kod ,,aplikacji'' w pliku hello1.c Aby ją zbudowac warto skorzystać z pkg-config, które odrobi za nas ustawienie ścieżek do kompilacji i linkowania (tak, tak..., to fragment Makefile):

CFLAGS=-Wall -Wunused -DG_DISABLE_DEPRECATED -DGDK_DISABLE_DEPRECATED \
-DGDK_PIXBUF_DISABLE_DEPRECATED -DGTK_DISABLE_DEPRECATED

gcc hello1.c -o hello1 $(CFLAGS) `pkg-config gtk+-2.0 --cflags --libs`
Warto zdefiniować flagi jak powyżej, aby ustrzec się używania przestarzałych (i często błędnych) funkcji i typów, trzymanych ze względu na kompatybilność.

W katalogu prog są także przykłady do kolejnych punktów.

2. Menu

Skoro mamy już działający program, zaczniemy go krok po kroku rozbudowywać (metoda iteracyjna -- można nawet powiedzieć zwinna) tak, aby otrzymać prostą przeglądarkę do plików. Zaczniemy od dodania menu.

Wiele kontenerów w GTK pozwala umieścić w nich tylko jeden obiekt (tzn. umieścić można więcej, ale nie będzie ich widać). Tak jest również dla okna głównego (GtkWindow). Można temu zaradzić umieszczając w nich jeden z kontenerów rozmieszczających (layout).

Ponieważ chcemy tylko mieć listwę menu u góry, a pod nią obszar roboczy, użyjemy pudełka. Pudełka dzielą się na pionowe i poziome; rozmieszczają obiekty jeden pod drugim lub jeden obok drugiego. Skorzystamy z pudełka pionowego (GtkVBox)

  GtkWidget *vbox;

  vbox = gtk_vbox_new(FALSE, 0);
  gtk_container_add(GTK_CONTAINER(window), vbox);
Parametry określają, czy obiekty zawarte wewnątrz mają mieć jednakowy przydział miejsca oraz jaki ma być odstęp między nimi (w pikselach).

Zanim zaczniemy rozmieszczać obiekty w pudełku trzeba je utworzyć. Na początek listwa menu (GtkMenuBar)

  GtkWidget *menubar;

  menubar = gtk_menu_bar_new();

Listwa zawiera elementy menu (GtkMenuItem), po wybraniu których wyświetlają się zwykłe menu (GtkMenu), także składające się z elementów. Nam wystarczy jedno menu z jednym elementem

  GtkWidget *file_item, *quit_item;
  GtkWidget *file_menu;

  file_item = gtk_menu_item_new_with_label("File");
  file_menu = gtk_menu_new();
  quit_item = gtk_menu_item_new_with_label("Quit");

Trzeba to wszystko połączyć razem. Zarówno listwa jak i zwykłe menu realizują interfejs GtkMenuShell.

  gtk_menu_shell_append(GTK_MENU_SHELL(menubar), file_item);
  gtk_menu_item_set_submenu(GTK_MENU_ITEM(file_item), file_menu);
  gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), quit_item);

Trzeba będzie jeszcze określić, co ma się dziać po wybraniu (jedynego) elementu menu. Wybranie elementu menu generuje sygnał "activate"

  g_signal_connect(G_OBJECT(quit_item), "activate",
                   G_CALLBACK(gtk_main_quit), NULL);
Uwaga dla purystów: to jest paskudny skrót, bo Ale ponieważ program zaraz i tak się skończy, więc nikt tego nie zauważy (C jest cool).

W obszarze roboczym pozostawimy przycisk, możemy więc zacząć rozmieszczanie

  gtk_box_pack_start(GTK_BOX(vbox), menubar, FALSE, FALSE, 0);
  gtk_box_pack_start(GTK_BOX(vbox), button, TRUE, TRUE, 0);

Takie ręczne budowanie struktury menu (i palet narzędzi) jest dość pracochłonne. GTK zawiera dwa narzędzia do szybkiego budowania menu na podstawie opisu w XML. Jedno z nich to UI Manager, został on użyty w przykładowym edytorze.

3. Dialogi

Do nagłej komunikacji z użytkownikiem służą dialogi. Są to okna główne otwierane na chwilę i na ogół wyświetlające komunikaty lub pola do pobrania informacji. Umieszczone w nich przyciski powodują zakończenie dialogu.

Użyjemy dialogu wybierania pliku do sprawdzenia, jakiego typu jest plik. Dialog wybierania pliku (GtkFileChooserDialog) to obudowa dla widgetu wybierania pliku, który można również wbudować na stałe w okno (ale mało kto tak robi).

Po pierwsze, trzeba dialog utworzyć

  GtkWidget *chooser;

  chooser = gtk_file_chooser_dialog_new("File selection", NULL,
                                        GTK_FILE_CHOOSER_ACTION_OPEN,
                                        GTK_STOCK_OPEN, GTK_RESPONSE_ACCEPT,
                                        GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
                                        NULL);
Kolejne argumenty to tytuł dialogu, jego obiekt nadrzędny (do zignorowania), akcja do wykonania (tutaj znelezienie pliku do otwarcia), oraz obczujnienie: lista tekstów przycisków (napis lub standardowy identyfikator) i ich odpowiedzi (liczby, ale lepiej korzystać ze stałych standardowych). Przyciski zrobią się same.

Teraz dialog uruchamiamy, zwróci wartość związaną z naciśniętym przyciskiem.

  int resp = gtk_dialog_run(GTK_DIALOG(chooser));

  if (resp == GTK_RESPONSE_ACCEPT) {
    char *fname =
      g_strdup(gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(chooser)));

    printf("Wybrano %s\n", fname);
    g_free(fname);
  }

Jeszcze trzeba dialog zamknąć

  
  gtk_widget_destroy(chooser); 

Podłączymy wybieranie nazwy pliku do menu

  GtkWidget *open_item;

  open_item = gtk_menu_item_new_with_label(GTK_STOCK_OPEN);
  gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), open_item);

  g_signal_connect(G_OBJECT(open_item), "activate",
                   G_CALLBACK(choose_file), NULL);

W funkcji choose_file() należy wywołać dialog, a potem wydobyć rozszerzenie z otrzymanej nazwy pliku. Ale to już praca badawcza własna.

4. Edytor w GTK

Do edycji większych tekstów w aplikacjach GTK służy rodzina klas, której centralnym elementem jest widget GtkTextView. Edycja tekstów oparta jest na podejściu Model-View-Controller (MVC), pozwalającym oddzielić dane od ich prezentacji i od obsługi zdarzeń --- można wtedy mieć wiele prezentacji tych samych danych, np. w 3 oknach.

Poza oficjalną dokumentacją GtkTextView jest w sieci dobry tutorial z przykładami.

Modelem jest tu GtkTextBuffer, a za widok czyli prezentację odpowiada GtkTextView, on też odbiera sygnały o zdarzeniach interfejsu.

Początki są miłe. Aby otworzyć okno z załadowanym plikiem tekstowym wystarczy

  GtkWidget *text_view = gtk_text_view_new();
  GtkTextBuffer *buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(text_view));
  gchar *contents, *filename;

  // Pobieramy cały plik (ależ ten GLib jest miły)
  g_file_get_contents(filename, &contents, NULL, NULL);
  gtk_text_buffer_set_text(buffer, contents, -1);
  gtk_widget_show(text_view);
  // Ale trzeba oddać
  g_free(contents);

Bufor przechowuje pliki tekstowe w UTF-8 (o ile wiem, na UCS-2 ani UCS-4 raczej go nie namówimy). Ważne rozróżnienie: ilekroć mowa o przesunięciu (offset), chodzi o numer pozycji w znakach, natomiast indeks (index) oznacza odległość od początku w bajtach i dobrze byłoby, żeby wypadał na początku znaku UTF-8.

Możemy już zrealizować naszą przeglądarkę plików. Zamieniamy przycisk w obszarze roboczym na edytor.

  GtkWidget *text_view = gtk_text_view_new();

  gtk_box_pack_start(GTK_BOX(vbox), text_view, TRUE, TRUE, 0);

Zmieniamy akcję dla otwierania pliku. Po pierwsze, przekażemy jej edytor jako dodatkowy parametr. Do tej pory nie korzystaliśmy z tej możliwości, ale określając procedurę do obsługi sygnału można określić dodatkowy parameter, który będzie jej przekazywany.

  g_signal_connect(G_OBJECT(open_item), "activate",
                   G_CALLBACK(choose_file), text_view);

Po drugie, w samej akcji po uzyskaniu z dialogu nazwy pliku przepisujemy plik do bufora

  GtkTextBuffer *buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(text_view));
  gchar *contents;

  g_file_get_contents(filename, &contents, NULL, NULL);
  gtk_text_buffer_set_text(buffer, contents, -1);
  g_free(contents);

5. Przewijanie okna

Dla małych plików jest ok, ale dla większych okno nie mieści się na ekranie. Pora na dołączenie przewijania.

Przewijanie można zrobić ręcznie dodając suwaki (GtkScrollbar) do okna i organizując samodzielnie wymianę informacji. Ręce mogą rozboleć. Na szczęście jest gotowiec: kontener GtkScrolledWindow. Wkładamy do niego nasze okno i po kłopocie.

  GtkWidget *scroller;

  scroller = gtk_scrolled_window_new(NULL, NULL);
  gtk_box_pack_start(GTK_BOX(vbox), scroller, TRUE, TRUE, 0);
  gtk_container_add(GTK_CONTAINER(scroller), text_view);
Można dodatkowo określić, żeby suwaki pojawiały się tylko w razie potrzeby.

6. Bufor tekstu

Operowanie na fragmentach tekstu w buforze jest trudniejsze -- trzeba się zaprzyjaźnić z iteratorami. Można je ustawić na dowolne miejsce w buforze, a potem użyć do wstawiania, usuwania albo pobierania tekstu z bufora, na przykład poniższy kod

  GtkTextIter start, end;
  gchar* text;

  gtk_text_buffer_get_iter_at_offset(buffer, &start, 5); 
  gtk_text_buffer_get_iter_at_offset(buffer, &end, 8);
  text = gtk_text_buffer_get_text(buffer, &start, &end, FALSE);
pobiera znaki od piątego do ósmego i zwraca jako napis C (trzeba go będzie potem oddać używając g_free()).

Z modyfikacjami bywa gorzej. Iteratory są ,,delikatne'': podczas jakiejkolwiek zmiany są w zasadzie unieważniane. Można tego nie zauważyć, bo na przykład po wstawieniu nowego tekstu

  gtk_text_buffer_insert(buffer, &iter, "Nowy tekst", -1);
iteratora iter można dalej używać. Tak naprawdę został on unieważniony, ale natychmiast przywrócony odpowiednim sygnałem. Generalna zasada: iteratory w argumentach funkcji ,,przeżywają'', inne raczej nie.

Aby móc zachować informację o pozycji w buforze dobrzy ludzie wymyślili znaczniki (mark). Znaczniki podobnie jak iteratory wskazują określone miejsce, ale są trwałe. Przy zmianach co najwyżej przesuwają się w lewo lub w prawo (wybór kierunku określa grawitacja znacznika).

Znaczniki mogą mieć nazwy, dwa standardowe znaczniki to znacznik bieżącego położenia kursora "insert" oraz znacznik drugiego krańca wybranego tekstu (,,selekcji'') "selection-bound". Tych akurat nie należy przestawiać ręcznie, bo rozsychronizujemy mechanizm. Na przykład aby zmienić pozycję kursora używa się

  gtk_text_buffer_place_cursor(buffer, &iter);
które przestawia znacznik we wskazane iteratorem miejsce.

Aby dostać bieżące położenie kursora można użyć funkcji gtk_text_buffer_get_insert(), zwracającą znacznik "insert". Teraz możemy już wywołać gtk_text_buffer_get_iter_at_mark() i dostaniemy iterator w bieżącym położeniu kursora.

Pora na coś nowego. Poszukamy w wyświetlanym pliku kolejnych trzech pierwszych słów i wypiszemy je na standardowe wyjście.

  GtkTextIter start, end;
  int i;

  gtk_text_buffer_get_start_iter(buffer, &end);
  for (i = 0; i < 3; i++) {
    char *word;

    gtk_text_iter_forward_word_end(&end); 
    start = end;
    gtk_text_iter_backward_word_start(&start); 
    word = gtk_text_iter_get_text(&start, &end);
    g_print("%s\n", word);
    g_free(word);
  }

7. Etykiety

Etykiety (tags) określają zwykle sposób wyświetlania. Mogą być nakładane na dowolny fragment tekstu w buforze, na przykład aby wyświetlić go na czerwono. Z każdym buforem związane jest tablica etykiet.

Etykiety tworzymy funkcją gtk_text_buffer_create_tag(). Parametrami są: bufor, nazwa etykiety oraz ciąg par [nazwa atrybutu,wartość]. Kończymy przez NULL.

  gtk_text_buffer_create_tag(buffer, "red_fg", 
                             "foreground", "red", 
                             "weight", PANGO_WEIGHT_BOLD, NULL);
Funkcja zwraca obiekt GtkTextTag, ale zwykle posługujemy się nazwą etykiety.

Etykiety można nakładać na dowolny spójny fragment tekstu. Można to zrobić już podczas wstawiania tekstu

  gtk_text_buffer_insert_with_tags_by_name(buffer, &iter, "ostatnie", -1,
                                           "red_fg",  NULL);

Można także potem, ale wtedy zakres działania etykiety trzeba określić dwoma iteratorami

  gtk_text_buffer_apply_tag_by_name(buffer, "red_fg", 
                                    &start_iter, &end_iter);

Iteratory mają masę pożytecznych funkcji. Są między innymi funkcje operujące na słowach:

8. Dla ciekawskich

Trzy podstawowe biblioteki Gtk+ to: glib, gdk i gtk.

Fragmentem GLib jest GObject: obiekty dla języków, w których ich nie ma (na przykład C). Obejmuje także GSignal --- implementację sygnałów. Obiekty mogą mieć widoczne na zewnątrz atrybuty, tutaj nazywane nie bez powodu własnościami. Każda własność ma bowiem identyfikator, nazwę, opis, dziedzinę, wartość początkową oraz kod do odczytywania i nadawania wartości.

Gospodarką pamięci zajmuje się odśmiecacz oparty na licznikach odwołań. Każdy GObject przechowuje licznik odwołań do niego, gdy licznik zmaleje do zera to obiekt znika.

Systemu typów dostarcza GType, opisujący własności typów i związki między nimi. Ponadto GValue to obudowa pozwalająca mieć wskaźniki z informacją o typie.

Marshaller to funkcja, która dla sygnałów przenosi parametry do procedury obsługi, a następnie odbiera i przenosi zwracaną wartość (wynik). Dla każdej sygnatury jest potrzebny osobny marshaller.

Funkcje z rodziny gtk_signal_connect() zwracają identyfikator handlera. Można go użyć do czasowego włączania i wyłączania handlera funkcjami gtk_signal_handler_block() i gtk_signal_handler_unblock().

Niektóre widgety nie mają własnego okna, więc nie dostają sygnałów z informacjami o zdarzeniach. Przykładem takiego widgetu jest etykieta (GtkLabel).

Aby mogły odpowiadać na zdarzenia należy je obudować kontenerem GtkEventBox, który będzie im przekazywał sygnały o zdarzeniach (bo jest wyposażony w okno).

Dane dla programu pkg-config są zwykle w katalogu /usr/libpkgconfig jako pliki o rozszerzeniu .pc, np. gtk+-2.0.pc. Mogą jednak być rozmieszczone w wielu katalogach, wtedy zmienna PKG_CONFIG_PATH podaje ich listę rozdzieloną dwukropkami. Najważniejsze opcje:

pkg-config --list-all

Podaje wszystkie znane pakiety i ich opisy.

pkg-config --modversion <pakiet>

Podaje numer wersji pakietu (natomiast --version podaje numer wersji pkg-config :-).

pkg-config --cflags <pakiety>

Podaje opcje do kompilacji (zwykle katalogi z plikami nagłówkowymi)

pkg-config --libs <pakiety>

Podaje opcje do linkowania (zwykle katalogi z bibliotekami i same biblioteki). Te dwie opcje można podawać razem.