W sieci są różne tutoriale do Gtk+, np. ZetCode.
Programy przykładowe opisywane w tekście znajdują się tu
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:
Nawiązujemy łączność z modułami GTK
gtk_init(&argc, &argv);GTK skonsumuje swoje specyficzne argumenty, a resztę pozostawi.
Otwieramy co najmniej jedno okno główne
GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL);Z każdą klasą widgetu związany jest typ w C (np.
GtkWindow
),
jednak konstruktory na ogół zwracają wskaźnik do typu GtkWidget
,
z którego dziedziczą inne typy Widgetów. Ułatwia to życie w pewnych
sytuacjach, powoduje jednak w zamian udekorowanie programu sporą liczbą
castów.
Definiujemy inne obiekty, na przykład przycisk
GtkWidget *button = gtk_button_new_with_label("Witamy na IPP");
Aby obiekty mogły być widoczne, należy je dołączyć do nadrzędnego kontenera i pokazać
gtk_container_add(GTK_CONTAINER(window), button); gtk_widget_show(button);Zwróćmy uwagę na użycie zamiast castu z C (np.
(GtkContainer *)window
)
makra GTK_CONTAINER()
. Sprawdzi ono, czy koercja typu jest
legalna. Uwaga: główne okno programu pokazujemy zwykle na końcu,
gdy już wszystko jest gotowe.
Pozostało jeszcze zadbanie, żeby program reagował na jakieś zdarzenia. Zniszczenie lub zamknięcie głównego okna powinna skutkować wyjściem z głównej pętli
g_signal_connect(window, "destroy", G_CALLBACK(destroy), NULL);Zapis powyższy oznacza, że gdy obiekt window otrzyma sygnał "destroy", powinna być wywołana funkcja
destroy()
, np. taka
static void destroy (GtkWidget *widget, gpointer data) { gtk_main_quit(); }Jej parametry to okno i dodatkowe dane ustawiane jako czwarty argument podłączania sygnału (tutaj
NULL
). Funkcja
gtk_main_quit()
kończy główną pętlę GTK.
Przycisk też może mieć obsługę sygnału, np.
g_signal_connect(button, "clicked", G_CALLBACK(hello), NULL);z funkcją
static void hello (GtkWidget *widget, gpointer data) { printf("Hello World\n"); }
Pora na uruchomienie zabawki
gtk_widget_show_all(window); gtk_main();Funkcja
gtk_widget_show_all()
jest przeznaczona dla
pracowitych inaczej. Wyświetla argument i wszystkie w nim zawarte
widgety.
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.
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
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.
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.
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);
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.
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); }
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:
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.