140 91 5MB
Polish Pages [508] Year 1999
Rozdział 1 Wprowadzenie do GTK+ GTK+ jest skrótem od GIMP Toolkit, natomiast GIMP jest skrótem nazwy programu narzędziowego, Graphical Image Manipulation. GIMP Toolkit to biblioteka, wykorzystywana do pisania aplikacji z graficznym interfejsem użytkownika (GUI, Graphical User Interface). Biblioteka jest obecnie szeroko stosowana podczas tworzenia aplikacji GUI w Linuksie. Program GIMP, który stanowi doskonały przykład profesjonalnej aplikacji GUI, został napisany właśnie w oparciu o bibliotekę GTK+. GIMP jest darmową aplikacją, o swobodnie dostępnym kodzie źródłowym, którą można pobrać spod adresu www.gimp.org. GTK+ można używać także do tworzenia aplikacji na innych platformach, na których jest dostępna. Jest to biblioteka obiektowa, napisana w C, która może wspierać aplikacje napisane w kilku różnych językach; obecnie na liście obsługiwanych języków są Ada95, C++, Eiffel, Objective C, Perl, Python i wiele innych. Aktualna lista powiązań językowych znajduje się na stronie WWW GTK+, www.gtk.org.
Założenia dotyczące czytelnika tej książki W książce zakładamy, że Czytelnik zna język programowania C i wie co nieco o Linuksie. Powinien mieć wprawę w posługiwaniu się wskaźnikami i zarządzaniu pamięcią. Czytelnik powinien także mieć doświadczenie w tworzeniu aplikacji GUI i orientować się w systemach zdarzeniowych. W kilku słowach: książka ta przeznaczona jest dla programistów Linuksa, którzy chcą tworzyć programy GUI. Książka przyda się także wszystkim tym, którzy programowali w innych systemach operacyjnych (na przykład MS Windows), a chcą programować aplikacje GUI w Linuksie.
Co zawiera ta książka? Książka opisuje posługiwanie się biblioteką GTK+, łącznie z GLIB i GDK, przy użyciu C. Nie oznacza to, że opisane zostaną wszystkie aspekty
4
Część I Programowanie w GTK+
GTK+; biblioteka ta jest o wiele za duża, żeby można było omówić ją w jednej książce. Celem tej książki jest uświadomienie programistom, że GTK+ jest pakietem narzędziowym GUI, służącym do tworzenia aplikacji w Linuksie i innych systemach operacyjnych. Biblioteka GTK+ jest oprogramowaniem darmowym, czy też raczej o otwartym źródle, co oznacza, że można pobrać z sieci jej pełen kod źródłowy. Istnieje tylko jedno ograniczenie wykorzystania tego kodu: wszystkie biblioteki stworzone na bazie GTK+ muszą także stosować się do licencji otwartego źródła. Licencja GTK+ ma znaczące konsekwencje. Ponieważ dostępny jest kod źródłowy, biblioteka ta nigdy nie stanie się martwym produktem, dopóki będą używać jej twórcy oprogramowania. Dzięki dostępności źródeł każdy programista może usunąć usterki w używanej przez siebie wersji, a następnie zgłosić poprawki osobom utrzymującym projekt GTK+ w celu dołączenia ich do głównej gałęzi projektu. Podejście to zmienia wielu programistów używających GTK+ w programistów samego GTK+, prowadząc do zwiększenia stabilności i wydajności biblioteki.
Jak można uzyskać GTK+? GTK+ można pobrać ze strony WWW GTK+, pod adresem www.gtk.org. Numerowanie wersji przypomina schemat znany z Linuksa: numery parzyste, 1.0 lub 1.2, są wersjami stabilnymi, natomiast numery nieparzyste, .99 lub 1.1, są wersjami rozwojowymi. Dodatkowy numer (na przykład 1.0.5) oznacza, że do danej wersji dodano poprawki.
W jaki sposób skompilować GTK+? Aby skompilować GTK+, należy najpierw pobrać GLIB i GTK+ ze strony WWW GTK+. Najpierw należy skompilować GLIB, ponieważ GTK+ wykorzystuje GLIB w wielu wewnętrznych funkcjach. Po skompilowaniu i zainstalowaniu GLIB można przystąpić do kompilacji GTK+.
Jestem nowicjuszem - jak skompilować kod źródłowy? Pakiety GLIB i GTK+ można skompilować w kilku prostych krokach. Pobrane pliki będą miały nazwy w rodzaju GTK+-1.2.0.tar.gz - jest to plik zarchiwizowany i skompresowany programami tar i gzip, na co wskazuje rozszerzenie .tar.gz. Najpierw należy rozpakować plik przy pomocy programu gunzip, wywoływanego z wiersza poleceń w następujący sposób:
Wprowadzenie do GTK+
5
gunzip GTK+-1.2.0.tar.gz W wyniku otrzymamy dużo większy plik archiwalny, o nazwie GTK+1.2.0.tar. Można teraz rozpakować go z zachowaniem właściwej struktury katalogów, przy pomocy polecenia: tar xvf GTK+-1.2.0.tar [przyp. tłumacza: w nowszych wersjach Linuksa, wystarczy pojedyncze polecenie tar zxvf GTK+-1.2.0.tar.gz] Polecenie to utworzy wszystkie katalogi, niezbędne do skompilowania biblioteki. Teraz należy wejść do nowo utworzonego katalogu i wpisać ./configure aby uruchomić program konfiguracyjny. Tworzy on plik makefile, z pomocą którego można skompilować bibliotekę. Jeśli nie zgłosi on żadnych błędów, wystarczy wpisać: make aby skompilować bibliotekę. Następnie trzeba ją zainstalować, do czego niezbędne są uprawnienia roota. Polecenie, które instaluje bibliotekę w systemie, to make install Aby biblioteka została rozpoznana przez system, konieczne może okazać się uruchomienie programu /sbin/ldconfig. Oczywiście, przed kompilacją i instalacją GTK+ należy skompilować i zainstalować GLIB.
Co jest potrzebne do uruchomienia przykładów? Przykłady zostały skompilowane w Linuksie (wersja jądra 2.0) przy pomocy kompilatora gcc w wersji 2.7.2. Nie testowano kompilatora egcs, który jednak również powinien działać. Przykłady pracują w wersji 1.1.5 GTK+ i powinny pracować w wersji 1.2. Wiele spośród nich nie uruchomi się z GTK+ 1.0.x bez zmian w kodzie źródłowym. Zakładając, że wszystko jest właściwie skonfigurowane, wystarczy wejść do jednego z przykładowych katalogów i wpisać make, aby skompilować program.
Skąd wziąć przykładowe programy? Ci, których nie bawi ślęczenie nad klawiaturą, mogą pobrać przykłady spod następujących adresów: http://www.mcp.com/product_support/
6
Część I Programowanie w GTK+
www.robomatic.ip.pl Dostępny jest kod źródłowy wszystkich przykładowych programów w książce. Wszystko, czego potrzeba, to GTK+ 1.2.
Gdzie zwrócić się o pomoc? Oprócz tej książki, istnieje kilka innych źródeł pomocy. Strona WWW GTK+ zawiera podręcznik oraz kilka przykładowych programów dla GTK+, nie licząc repozytorium innych aplikacji GTK+, które mogą okazać się bardzo pomocne w nauce. W źródłach GTK+ znajduje się katalog przykładów, zawierający aplikacje demonstrujące różne aspekty programowania GTK+. Aplikacja testGTK+ może posłużyć do przetestowania wszystkich kontrolek (ang. widgets) w GTK+, a jej kod źródłowy może okazać się bezcenny, kiedy nie można znaleźć dokumentacji dla jakiejś kontrolki, ponieważ prawdopodobnie jest ona uwzględniona w testGTK+. Oczywiście, dla odważnych zawsze dostępny jest pełny kod źródłowy GTK+. Jeśli w żadnych z tym źródeł nie znajdziemy odpowiedzi na nasze pytania, możemy spróbować różnych list wysyłkowych, dotyczących GTK. Doświadczeni twórcy aplikacji GTK+ odpowiedzą na nasze pytania, od prostych do bardzo złożonych. Główne listy wysyłkowe to [email protected] i [email protected]. Aby zapisać się na listę GTK-list, należy wysłać wiadomość do [email protected] ze słowem „subscribe” w temacie. Podobnie, aby zapisać się na listę GTK-app-devel-list, należy wysłać wiadomość do GTK-app-devel-listrequest@ redhat.com ze słowem „subscribe” w temacie. Należy zdawać sobie sprawę, że na obu listach, a zwłaszcza GTK-list, panuje spory ruch około 50 wiadomości e-mail dziennie, a czasami nawet więcej.
Rozdział 2 GLIB Biblioteka GLIB jest zbiorem funkcji, intensywnie używanych przez GTK+. Łączone listy, drzewa, obsługa błędów, zarządzanie pamięcią i zegary to tylko część zawartości biblioteki. Rozdział ten omawia najczęściej wykorzystywane funkcje biblioteki GLIB. GTK+ wymaga obecności biblioteki GLIB, od której uzależniona jest jej przenośność i funkcjonalność, natomiast biblioteka GLIB może być używana samodzielnie, do tworzenia aplikacji bez graficznego interfejsu użytkownika.
Typy Zamiast korzystać ze standardowych typów języka C, biblioteka GLIB wprowadza własny zbiór typów. Podejście takie ułatwia przenoszenie biblioteki na inne platformy i pozwala na zmianę typów danych bez przepisywania aplikacji. Ponieważ GTK+ korzysta z typów danych i funkcji biblioteki GLIB, przeniesienie GTK+ wymaga przeniesienia GLIB na docelową platformę, a także przystosowanie jej do biblioteki GDK, używanej przez GTK+. Przenoszenie aplikacji pomiędzy platformami wymaga sporej dozy cierpliwości, niezależnie od przyjętych założeń projektowych, zwłaszcza, jeśli oprogramowanie przenoszone jest pomiędzy niepodobnymi do siebie platformami (Linux Æ Windows, w przeciwieństwie do Linux Æ Unix). GLIB korzysta z wielu typów danych, które różnią się nieco od standardowych typów C. Typ danych char z C jest typem gchar w GLIB. Chociaż gchar może być zdefiniowany jako char w plikach nagłówkowych GLIB przeznaczonych dla platformy Intela, to typów tych nie należy używać zamiennie, jeśli ma być zachowana przenośność aplikacji. Wiele funkcji przyjmuje jako parametr typ gchar * zamiast char *. Zmiana ta jest nieznaczna, ale trzeba się do niej przyzwyczaić. Najczęściej używane, zmodyfikowane typy to: 0Typy C
Typy GLIB
char
gchar
Część I Programowanie w GTK+
8 short
gshort
long
glong
int
gint
char
gboolean
void *
gpointer
Używanie typów danych GLIB w aplikacjach GTK+/GLIB pozwala mieć pewność, że aplikacja będzie działać, kiedy zmieni się implementacja bazowego typu danych (np. gboolean). Na przykład, w późniejszej wersji biblioteki typ danych gboolean może zostać zdefiniowany jako int; dzięki użyciu typu gboolean zamiast char, program nadal będzie kompilował się bezproblemowo.
Komunikaty GLIB posiada cztery funkcje do wyświetlania komunikatów, z których każda może zostać rozszerzona „w locie” w zależności od tego, czy tworzymy aplikację z interfejsem GUI, czy też bez niego. Funkcje te implementują cztery poziomy obsługi komunikatów, od nienaprawialnego błędu wyświetlanego przez g_error do standardowej funkcji wyjściowej g_print. Każda z nich wyświetla inny typ komunikatu i może przyjmować zmienną liczbę parametrów, podobnie jak funkcja printf.
g_error Funkcja g_error służy do wyświetlania krytycznych błędów w aplikacji. Wyświetla komunikat i przerywa pracę programu. Funkcji należy używać tylko w przypadku tych błędów, które i tak spowodowałyby zakończenie programu. Funkcja g_set_error_handlermoże zmienić zachowanie funkcji g_error, ale nie może powstrzymać jej od przerwania programu.
g_warning Funkcja g_warning wyświetla komunikat o zajściu naprawialnego błędu, pozwalając na dalszą pracę programu. GTK+ korzysta z niej w celu wyświetlenia komunikatów o błędach programowych, które zostały pomyślnie obsłużone. Funkcja g_set_warning_handler może zmienić domyślne zachowanie funkcji g_warning.
9
GLIB
g_message Funkcja g_message wyświetla komunikaty o zdarzeniach niezwiązanych z błędami. Funkcja g_set_message_handler może zmienić zachowanie funkcji g_message.
g_print Funkcja g_print używana jest głównie podczas wykrywania i usuwania usterek. Można używać funkcji g_print w czasie tworzenia programu, a w ostatecznej wersji zmienić jej zachowanie tak, aby nie wyświetlała żadnych komunikatów. Podejście takie jest szybkie, łatwe, i nie wymaga przeglądania kodu w celu usunięcia „odpluskwiaczy”. Do zmiany zachowania funkcji g_print służy funkcja g_set_print_handler. Można zmienić (przeciążyć) zachowanie każdej z opisanych funkcji, przekazując nazwę nowej procedury obsługi komunikatu. Procedura taka mogłaby na przykład wyświetlić okno dialogowe „Zapisz komunikat/błąd w pliku” lub wykonać dowolną inną czynność. Zachowanie funkcji wyświetlających komunikaty można więc szybko zmieniać w trakcie pisania programu, dostosowując je do swoich potrzeb.
Własne procedury obsługi błędów Poniższy przykład ilustruje używanie własnych procedur obsługi błędów. Uruchomienie programu z parametrem normalny powoduje, że wykorzystane zostaną standardowe funkcje komunikatowe z GLIB: [bystry@umysl komunikaty]$ komunikat normalny Oto wydruk message: Oto komunikat ** WARNING **: Oto ostrzeżenie ** ERROR **: Oto błąd Aborted (core dumped)
Uruchomienie programu z parametrem surfer powoduje, że domyślne funkcje komunikatowe wyświetlają te same teksty w nieco odmienny sposób. Zauważmy jednak, że program nadal przerywa pracę, kiedy wystąpi błąd, mimo zainstalowania własnej procedury obsługi błędu. [bystry@umysl komunikaty]$ komunikat surfer Koleś, oto wydruk Koleś, dostałeś komunikat - Oto komunikat
10
Część I Programowanie w GTK+
Złe wieści, koleś - Oto ostrzeżenie Kompletny pad, koleś - Oto błąd Aborted (core dumped) [bystry@umysl komunikaty]$ exit
Poniżej zamieszczamy kod programu. Jeśli zostanie on uruchomiony z argumentem surfer, wówczas przed wywołaniem funkcji komunikatowych ustawiane są nowe procedury obsługi komunikatów. /* * komunikat.c * * Przykład ilustrujący funkcje komunikatowe */ #include /* * SurferPrint * * Funkcja przeciążająca g_print */ void SurferPrint (const gchar *buf) { printf ("Koleś, "); printf (buf); } /* * SurferMessage * * Funkcja przeciążająca g_message */ void SurferMessage (const gchar *buf) { printf ("Koleś, dostałeś komunikat - "); printf (buf); } /* * SurferWarning * * Funkcja przeciążająca g_warning
11
GLIB
*/ void SurferWarning (const gchar *buf) { printf ("Złe wieści, koleś - "); printf (buf); } /* * SurferError * * Funkcja przeciążająca g_error */ void SurferError (const gchar *buf) { printf ("Kompletny pad, koleś - "); printf (buf); } /* * PokazParametry * * Pokazuje dostępne opcje programu */ void PokazParametry () { printf ("Konieczne jest podanie parametru. Dostępne parametry to:\n"); printf (" 'surfer' - używa obsługi komunikatów w stylu surfera\n"); printf (" 'normalny' - używa zwykłej obsługi komunikatów.\n "); exit (0); } /* * main * * Tu zaczyna się program */ int main (int argc, char *argv[]) { /* --- Za mało argumentów? --- */ if (argc data), a nie zawartość wskazywanych przez nie lokacji. Należy o tym pamiętać, jeśli używamy list do przechowywania łańcuchów. Oprócz tego, konieczne może się okazać zwolnienie pamięci zajmowanej przez dane, jeśli została ona przydzielona przez programistę.
Pobieranie n-tego elementu Można pobrać n-ty element listy, używając funkcji g_slist_nth i przekazując jej indeks żądanego elementu listy. Poniższy przykład pobiera siódmy element listy: wezel = g_slist_nth (lista, 7);
Można sprawdzić indeks pola danych przy pomocy funkcji g_slist_index. Ponieważ funkcja ta wywołuje funkcję g_slist_find, stosują się do niej wszystkie ostrzeżenia, o których wspomniano we wcześniejszym podrozdziale, „Szukanie elementów na liście”. nIndeks = g_slist_index (lista, 22);
Przeglądanie listy Pierwszym sposobem przejrzenia elementów łączonej listy jest ręczne przejście przez jej zawartość przy pomocy pętli. wezel->next powoduje przejście do następnego węzła łączonej listy. /* --- ‘lista’ oznacza czoło listy --- */ /* --- Przechodzimy przez listę w pętli --- */ for (wezel = lista; wezel; wezel = wezel->next) { /* Wyświetlamy zawartość, przy założeniu, że dane są typu char */ g_print ("%s\n", (char *) wezel->data); }
Drugi sposób polega na wykorzystaniu funkcji g_slist_foreach. Funkcja ta wywołuje inną funkcję i przekazuje jej element danych z każdego węzła łączonej listy. Aby użyć funkcji g_slist_foreach musimy najpierw stworzyć funkcję (na przykład WypiszImiona), która będzie dokonywała właściwych operacji na polu danych. Funkcja WypiszImiona pobiera dane
19
GLIB
i wyświetla je. Parametr dane oznacza dane do wyświetlenia, przekazywane z każdego elementu listy. Parametr dane_uzytkownika oznacza dodatkowe informacje, które można przekazać wraz z każdym elementem. void WypiszImiona (gpointer dane, gpointer dane_uzytkownika) { gchar *komunikat; /* --- Zamieniamy dane na łańcuch --- */ komunikat = (gchar *) dane; /* --- Wyświetlamy łańcuch --- */ g_print ("%s\n", komunikat); }
Chociaż funkcja ta wyświetla tylko jeden element listy, to funkcja g_slist_foreach wywoła ją kolejno z każdym elementem. Można ustawić funkcję g_slist_foreach tak, aby wywoływała funkcję WypiszImiona, w następujący sposób: /* --- Inna metoda wypisania wszystkich pól danych --- */ /* --- Wywołujemy WypiszImiona dla każdego elementu, --- */ /* --- dane_uzytkownika = NULL --- */ g_slist_foreach (lista, (GFunc) WypiszImiona, NULL);
W tym przykładzie, w parametrze dane_uzytkownika funkcji WypiszImiona dla każdego elementu listy zostanie przekazane NULL. Parametr ten można wykorzystać w celu przekazania innych informacji, których mogłaby potrzebować funkcja WypiszImiona.
Usuwanie listy Aby usunąć całą listę, wywołujemy funkcję g_slist_free z argumentem w postaci listy. g_slist_free (lista);
Jeśli pola danych są wskaźnikami do przydzielonej pamięci, należy zwolnić tę pamięć przed wywołaniem g_slist_free; w przeciwnym przypadku pamięć zostanie stracona.
Listy dwukierunkowe GLIB posiada także zbiór funkcji operujących na listach dwukierunkowych, które wykorzystują typ danych GList. Funkcje te przypominają
Część I Programowanie w GTK+
20
w działaniu funkcje operujące na listach jednokierunkowych, ale wszystkie posiadają w nazwie przedrostek g_list zamiast g_slist, i używają typu danych GList zamiast GSList. Typ danych GList posiada połączenie z następnym i poprzednim elementem listy, co znacząco ułatwia poruszanie się w tył listy (patrz rysunek 2.2).
Rysunek 2.2. GList.
Wydajność łączonych list Łączone listy nadają się do implementacji stosów (w których elementy są dodawane i usuwane z początku listy) oraz niewielkich list, ale są dość kosztowne w użyciu, jeśli przechowują duże ilości danych. Jeśli używa się list do przechowywania informacji, należy dodawać dane na początek listy, dzięki czemu wstawianie elementów będzie przebiegać bardzo szybko. Przeszukiwanie listy w celu znalezienia danych również może okazać się kosztowne, proporcjonalnie do długości listy.
21
GLIB
Tablice przemieszczania GLIB posiada zbiór funkcji, operujących na tablicach przemieszczania (ang. hash tables). Tablice takie umożliwiają szybkie dodawanie i pobieranie informacji: elementy są dodawane przy użyciu klucza, który służy do późniejszego pobrania wartości. Rysunek 2.3 ilustruje sposób przechowywania wartości w tablicach przemieszczania. Tablice przemieszczania wymagają napisania funkcji zwrotnej, która będzie obliczać wartość skrótu (hash value). Powinna ona zwracać możliwie unikalną wartość.
Tworzenie tablicy przemieszczania Tablice przemieszczania tworzymy przy pomocy funkcji g_hash_table_ new, która zwraca wskaźnik do GHashTable. Wymaga ona określenia funkcji mieszającej (hashing function) oraz funkcji porównującej (comparison function). Funkcja mieszająca oblicza wartość skrótu (hash value), która określa, do jakiej komórki tablicy (bucket) zostanie dodany klucz. Funkcja porównująca porównuje klucze podczas szukania wartości w tablicy. Jeśli w tablicy przemieszczania zamierzalibyśmy przechowywać łańcuchy, wówczas prosta funkcja mieszająca mogłaby pobierać dwa pierwsze znaki łańcucha i dodawać je do siebie, tworząc w ten sposób skrót do klucza: /* * Tworzymy skrót na podstawie dwóch pierwszych znaków */ guint FunkcjaMieszajaca (gpointer klucz) { char *sKlucz; sKlucz = klucz; return (sKlucz[0] + sKlucz[1]); }
Część I Programowanie w GTK+
22
Rysunek 2.3. Tablice przemieszczania.
Jednakże funkcja tego rodzaju jest nie najlepszą funkcją mieszającą; dla słów linux, linus, lizak i lista wartość skrótu byłaby taka sama, ponieważ funkcja oblicza ją na podstawie dwóch pierwszych znaków łańcucha. Funkcja mieszająca powinna robić lepszy użytek z danych i zwracać bardziej unikalny skrót. Kolejna przykładowa funkcja mieszająca używa całego łańcucha, aby obliczyć skrót; pracuje jednak dłużej, niż poprzednia funkcja mieszająca: guint FunkcjaMieszajaca (gpointer klucz) { char *sKlucz; guint giSkrot = 0; int nIndeks sKlucz = klucz; /* --- obsługa błędów --- */ if (klucz == NULL) return (0); /* --- obliczamy skrót na podstawie całego łańcucha --- */ for (nIndeks = 0; nIndeks < strlen (sKlucz); nIndeks++) { /* --- przesuwamy w lewo, ponieważ kolejność jest istotna --- */ giSkrot = (giSkrot entry), "changed", GTK_SIGNAL_FUNC (funkcja_combo), NULL);
Kontrolka GtkCombo nie posiada żadnej funkcji do czyszczenia zawartości, ale zawiera kontrolkę GtkList, która przechowuje elementy rozwijanej listy. Możemy skorzystać z tego faktu, aby manipulować elementami GtkCombo. Wyczyszczenie rozwijanej listy polega po prostu na pobraniu wewnętrznego pola listy i skorzystaniu z funkcji gtk_list_clear_items. GtkList *lista; /* --- pobieramy listę z listy kombinowanej --- */ lista = GTK_LIST (GTK_COMBO (combo)->list); /* --- czyścimy listę wewnątrz listy kombinowanej --- */ gtk_list_clear_items (lista, 0, -1);
Możemy także wyczyścić listę pojedynczą instrukcją: gtk_list_clear_items (GTK_LIST (GTK_COMBO (combo)->list), 0, -1);
76
Część I Programowanie w GTK+
Przycisk menu Przyciski menu (GtkOptionMenu) przypominają GtkList bez możliwości edycji (patrz rysunek 4.5). Chociaż ich działanie jest podobne, to wygląd jest zupełnie odmienny. Przyciski menu są czasem używane w sposób zbliżony do GtkCombo. Po kliknięciu przycisku GtkOptionMenu pojawia się menu, przedstawiające użytkownikowi dopuszczalne opcje. Wybranie którejś opcji powoduje, że po zniknięciu menu pojawia się ona na przycisku. Należy najpierw utworzyć GtkOptionMenu przy pomocy funkcji gtk_option_menu_new. Następnie trzeba utworzyć kontrolki GtkRadioMenuItem, dodać je do grupy (tak, jak przyciski opcji), a następnie dodać je do GtkMenu. Po wypełnieniu GtkMenu można związać je z GtkOptionMenu. Inicjacja GtkOptionMenu wygląda mniej więcej w ten sposób: /* --- tworzymy przycisk menu --- */ pmenu = gtk_option_menu_new (); /* --- tworzymy menu --- */ menu = gtk_menu_new (); /* --- na razie nie ma żadnej grupy --- */ grupa = NULL;
Każdy element dodawany do przycisku menu musi przejść przez następujący proces: /* --- wyświetlana wartość --- */ sTekst = "Średni"; /* --- tworzymy element menu z etykietą --- */ elmenu = gtk_radio_menu_item_new_with_label (grupa, sTekst); /* --- pobieramy grupę, w której jest element menu --- */ grupa = gtk_radio_menu_item_group (GTK_RADIO_MENU_ITEM (elmenu)); /* --- dodajemy element do menu --- */ gtk_menu_append (GTK_MENU (menu), elmenu); /* --- uwidaczniamy element --- */ gtk_widget_show (elmenu); /* --- trzeba powiadamiać o wybraniu elementu i przekazywać --- */ /* --- tekst do funkcji zwrotnej --- */
Podstawowe kontrolki
77
gtk_signal_connect_object (GTK_OBJECT (elmenu), "activate", GTK_SIGNAL_FUNC (wybrano_el_menu), (gpointer) sTekst);
Sygnał "activate" wskazuje, że wybrano kontrolkę GtkRadioMenuItem. Po dodaniu wszystkich elementów do menu, można związać je z przyciskiem menu w taki oto sposób: /* --- wiążemy menu z przyciskiem menu --- */ gtk_option_menu_set_menu (GTK_OPTION_MENU (pmenu), menu); /* --- uwidaczniamy przycisk menu --- */ gtk_widget_show (pmenu);
GtkOptionMenu przydaje się w sytuacji, kiedy chcemy dać użytkownikowi listę opcji do wyboru i wyświetlać aktualnie wybraną opcję.
Pojemniki Pojemniki (GtkContainer) są kontrolkami, które mogą być rodzicami dla innych kontrolek, umieszczanych w kontrolce pojemnika. Nie można stworzyć kontrolki pojemnika jako takiej, ponieważ nie istnieje funkcja gtk_container_new, ale kontrolek pojemnika używa się w programach aż nadto często. Przykładami kontrolek pojemnika mogą być główne okna z wcześniejszych przykładów, przyciski i pola list. Głównym zastosowaniem kontrolki pojemnika jest udostępnienie uniwersalnych funkcji, których można używać do operacji na wszelkich typach kontrolek pojemnika, bez potrzeby pisania specyficznego zbioru funkcji dla każdej kontrolki. Kontrolki można dodawać do pojemników przy pomocy funkcji gtk_container_add i usuwać przy pomocy gtk_container_remove. Większość kontrolek pojemnika pozwala na dodanie do nich tylko jednej kontrolki. Istnieje kilka wyjątków, na przykład GtkList, ale pojemniki te służą specjalnie do przechowywania wielu elementów. Jeśli zwykły pojemnik musi zawierać więcej niż jedną kontrolkę, wówczas trzeba skorzystać z pól albo tabel pakujących. Iterację listy kontrolek potomnych, przechowywanych w pojemniku, umożliwia funkcja gtk_container_children, która zwraca łączoną listę (GList *) kontrolek potomnych. Można także wykorzystać funkcję gtk_container_foreach, która wywołuje określoną funkcję dla każdej z kontrolek potomnych i przekazuje do niej tę kontrolkę jako parametr.
78
Część I Programowanie w GTK+
Funkcja gtk_container_border_width kontroluje szerokość obramowania wokół kontrolki, przechowywanej w pojemniku. Ustawienie szerokości obramowania na 0 powoduje, że kontrolka całkowicie wypełnia pojemnik.
Podsumowanie Podczas tworzenia kontrolek konieczne jest ustawienie ich właściwości, zanim zostaną wyświetlone na ekranie. Kontrolki posiadają rozmaite funkcje, które ustawiają ich atrybuty, a większość z nich sygnalizuje zdarzenia, które mogą zostać przechwycone przez aplikację. W rozdziale opisano etykiety, przyciski, pola list, pola wpisu i listy kombinowane. Są to kontrolki najczęściej używane w aplikacjach, a uzmysłowienie sobie ich działania jest konieczne do zrozumienia bardziej skomplikowanych kontrolek.
Rozdział 5 Menu, paski narzędziowe i podpowiedzi Dobry interfejs aplikacji jest często równie ważny, jak jej wnętrze. Interfejs jest pierwszą rzeczą, którą widzą użytkownicy - jeśli nie udostępnia on szybkiego i intuicyjnego sposobu przeprowadzenia żądanych czynności, oznacza to, że programiście nie udało się napisać dobrej aplikacji. Podobnie jak w przypadku samochodów, których powodzenie jest w równej mierze kwestią marketingu, jak pracy inżynierów, interfejs aplikacji jest równie ważny, jak jej wewnętrzne algorytmy. Zwłaszcza osoby nie parające się programowaniem często sądzą aplikację po jej wyglądzie i łatwości użycia, a nie po algorytmach. Użytkownicy przyzwyczaili się do dobrze zaprojektowanych menu i pasków narzędziowych, umożliwiających szybki dostęp do często wykorzystywanych funkcji. W rozdziale tym zaprojektujemy prosty interfejs przy pomocy funkcji GTK+. Znajdzie się w nim menu i pasek narzędziowy.
Uruchamianie aplikacji Należy stworzyć główne okno aplikacji i ustawić jego atrybuty. W oknach najwyższego poziomu zazwyczaj ustawia się takie atrybuty jak rozmiar, tytuł i obramowanie. Istotną sprawą jest tytuł aplikacji. Funkcja gtk_window_new tworzy okno najwyższego poziomu, w którym określamy charakterystyki aplikacji. okno = gtk_window_new (GTK_WINDOW_TOPLEVEL);
Po utworzeniu okna możemy używać uchwytu zwróconego przez gtk_window_new, aby ustawić cechy okna. Tytuł pozwala użytkownikowi odróżnić okno od innych okien na ekranie, i powinien być możliwie krótki, ale odpowiednio opisywać okno. Tytuł ustawiamy przy pomocy funkcji gtk_widget_set_title. gtk_widget_set_title (GTK_WINDOW (okno), "Tytuł aplikacji");
80
Część I Programowanie w GTK+
Jeśli aplikacja jest rodzajem edytora, wówczas tytuł okna powinien zawierać nazwę pliku, aby przypominał użytkownikowi, co właściwie edytuje. Pamiętajmy, że równocześnie może być otwarte wiele okien, a tytuł pomaga użytkownikowi w ich identyfikacji. Rozmiar okna wynika z rozmiaru kontrolek w nim zawartych. Czasem rozmiar określony przez kontrolki jest zbyt mały. W takim przypadku można ustalić z góry minimalny rozmiar okna, przy pomocy funkcji gtk_widget_set_usize. Jeśli utworzone okno będzie mniejsze, wówczas jego rozmiar będzie powiększony do minimalnej wielkości. Poniższa instrukcja ustawia wymiary okna na 300 x 200 pikseli: gtk_widget_set_usize (okno, 300, 200);
Szerokość obramowania okna można ustawić przy pomocy funkcji gtk_container_border_width. Poniższy przykład ustawia szerokość obramowania na zero: gtk_container_border_width (GTK_CONTAINER (okno), 0);
W rozdziale 3, „Tworzenie aplikacji GUI”, nauczyliśmy się uwidaczniać okna i oczekiwać na sygnał "delete_event"; należy przeprowadzić te operacje na oknie naszej aplikacji. Sygnał "delete_event" wskazuje, że okno jest zamykane. Kiedy okno jest gotowe, możemy zacząć dodawać do niego elementy interfejsu użytkownika, aby nasza aplikacja nabrała bardziej profesjonalnego wyglądu.
Menu (trudniejsza metoda) Menu w GTK+ są kontrolkami i zachowują się tak, jak wszystkie inne kontrolki. Menu, które zazwyczaj przychodzi nam na myśl, składa się z dwóch części: paska menu, umieszczonego w górnej części okna, oraz kilku menu, zwieszających się z tego paska. Pasek menu jest zazwyczaj najwyżej położoną kontrolką wewnątrz okna aplikacji. Układ, w którym pasek menu znajduje się na górze okna, a pod nim umieszczony jest pasek narzędziowy, sugeruje, że menu powinno być wstawione do pionowego pola pakującego. Musimy więc stworzyć pionowe pole pakujące, zanim będziemy mogli przystąpić do tworzenia menu. /* --- tworzymy pionowe pole pakujące w oknie aplikacji --- */ ypole = gtk_vbox_new (FALSE, 0); /* --- dodajemy pole pakujące do głównego okna --- */ gtk_container_add (GTK_CONTAINER (okno), ypole);
Menu, paski narzędziowe i podpowiedzi
81
/* --- uwidaczniamy pole pakujące --- */ gtk_widget_show (ypole);
Menu składa się z paska menu (GtkMenuBar), który spoczywa w poprzek całej górnej części okna, oraz poszczególnych menu (GtkMenu), które mogą być opuszczane z paska menu albo innych menu. Każda pojedyncza opcja menu jest elementem GtkMenuItem, indywidualnie tworzonym i dodawanym do GtkMenu. Pierwszym krokiem jest utworzenie GtkMenuBar i dodanie go do pionowego pola pakującego. Funkcja gtk_menu_bar_new tworzy kontrolkę GtkMenuBar. Oto kod, tworzący pasek menu i umieszczający go w pionowym polu pakującym: /* --- tworzymy pasek menu dla aplikacji --- */ pasekmenu = gtk_menu_bar_new (); /* --- umieszczamy pasek menu w polu pakującym --- */ gtk_box_pack_start (GTK_BOX (ypole), pasekmenu, FALSE, TRUE, 0); /* --- uwidaczniamy pasek menu --- */ gtk_widget_show (pasekmenu);
Kod ten tworzy pusty pasek menu. Aby pasek mógł się do czegoś przydać, należy dodać do niego elementy menu. Kiedy omawialiśmy podstawowe kontrolki, stworzyliśmy kilka procedur, które umożliwiały przeprowadzenie kilku operacji w pojedynczej funkcji. Tej samej techniki możemy użyć w przypadku menu. Najpierw należy utworzyć element menu „Plik” (GtkMenuItem) wraz z etykietą. /* --- tworzymy element menu dla opcji "Plik" --- */ menuPlik = gtk_menu_item_new_with_label ("Plik");
Po utworzeniu GtkMenuItem musimy dodać go do utworzonego wcześniej paska GtkMenuBar, a następnie uwidocznić. /* --- dodajemy element "Plik" do paska menu --- */ gtk_menu_bar_append (GTK_MENU_BAR (pasekmenu), menuPlik); /* --- uwidaczniamy kontrolkę --- */ gtk_widget_show (menuPlik);
W większości aplikacji, element menu „Plik” zawiera dalsze elementy menu, widoczne tylko wtedy, kiedy menu zostanie rozwinięte. Wybranie opcji „Plik” nie prowadzi do podjęcia żadnej czynności przez aplikację, tylko otwiera podmenu, które zawiera opcje typu „Nowy”, „Otwórz” lub „Zapisz”. Ponieważ element „Plik” w istocie prowadzi do innego menu,
82
Część I Programowanie w GTK+
należy związać go z nowym menu, zawierającym inne elementy. Po stworzeniu nowego GtkMenu, które będzie zawierać opcje „Nowy”, „Otwórz” i „Zapisz”, możemy związać je z elementem „Plik” przy pomocy funkcji gtk_menu_item_set_submenu. /* --- tworzymy nowe menu dla opcji "Nowy", "Otwórz" itp. --- */ menu = gtk_menu_new (); /* --- umieszczamy podmenu pod elementem "Plik" --- */ gtk_menu_item_set_submenu (GTK_MENU_ITEM (menuPlik), menu);
Teraz możemy dodać elementy GtkMenuItem do nowego GtkMenu. Aby uprościć procedurę dodawania podmenu, należałoby przenieść cały proces tworzenia podmenu do funkcji. /* * UtworzPodmenuPaska * * Funkcja tworzy podmenu * * menu - menu wyższego poziomu * szNazwa - nazwa elementu podmenu, który ma być dodany do menu */ GtkWidget *UtworzPodmenuPaska (GtkWidget *menu, char *szNazwa) { GtkWidget *elmenu GtkWidget *podmenu /* --- tworzymy element menu --- */ elmenu = gtk_menu_item_new_with_label (szNazwa); /* --- dodajemy je do paska menu --- */ gtk_menu_bar_append (GTK_MENU_BAR (menu), elmenu); gtk_widget_show (elmenu); /* --- tworzymy menu i dołączamy je do elementu menu --- */ podmenu = gtk_menu_new (); gtk_menu_item_set_submenu (GTK_MENU_ITEM (elmenu), podmenu); /* --- Voila! --- */ return (podmenu); }
Po dodaniu podmenu, można rozpocząć dodawanie do niego indywidualnych elementów GtkMenuItem. Zaczynamy od utworzenia GtkMenuItem.
Menu, paski narzędziowe i podpowiedzi
83
elmenu = gtk_menu_item_new_with_label ("Nowy");
Następnie dodajemy GtkMenuItem do właśnie utworzonego podmenu i czynimy go widocznym: gtk_menu_append (GTK_MENU (podmenu), elmenu); gtk_widget_show (elmenu);
Oczywiście, nasze menu jeszcze nic nie robi. Najpierw musimy przechwycić zdarzenia, sygnalizujące, że użytkownik wybrał jeden z elementów menu. GtkMenuItem posiada sygnał „activate”, który wskazuje, że wybrano GtkMenuItem. Ustanawiając funkcję zwrotną dla sygnału „activate” możemy sprawić, że aplikacja zareaguje na wybór opcji z menu. Można wprowadzić separator pomiędzy dwoma elementami menu GtkMenuItem, wywołując funkcję gtk_menu_item_new (wersja bez etykiety), i dodając otrzymany GtkMenuItem do podmenu. W menu pojawi się wówczas pozioma linia, rozdzielająca grupy poleceń; separatory tego typu są przydatne zwłaszcza w długich menu. Opcja menu „Zakończ” powinna być oddzielona od innych separatorem, aby użytkownik przypadkiem na nią nie trafił, wybierając sąsiednie opcje.
Zaznaczalne elementy menu Oprócz wykonywania poleceń, elementy menu mogą pokazywać także informacje o stanie programu. Zaznaczalny element menu (GtkCheckMenuItem) działa podobnie jak przycisk wyboru, pozwalając oglądać i ustawiać w menu Boole'owską wartość. Procedura tworzenia GtkCheckMenuItem jest podobna, jak w przypadku tworzenia zwykłego GtkMenuItem, z tym, że obecnie używamy funkcji gtk_check_menu_item_ new_with_label i przechwytujemy sygnał „toggled „. Możemy napisać funkcję, która wykona wszystkie czynności, potrzebne do utworzenia GtkCheckMenuItem: /* * UtworzZaznaczalnyElement * * Tworzy zaznaczalny element menu * * menu - menu macierzyste, do którego należy dodać element * szNazwa - nazwa przypisana do elementu menu * funkcja - procedura obsługi zdarzenia, wywoływana po zmianie * stanu elementu menu * dane - dodatkowe dane przekazywane do funkcji obsługi zdarzenia
84
Część I Programowanie w GTK+
*/ GtkWidget *UtworzZaznaczalnyElement (GtkWidget *menu, char *szNazwa, GtkSignalFunc funkcja, gpointer dane) { GtkWidget *elmenu; /* --- tworzymy element menu --- */ elmenu = gtk_check_menu_item_new_with_label (szNazwa); /* --- dodajemy go do menu --- */ gtk_menu_append (GTK_MENU (menu), elmenu); gtk_widget_show (elmenu); /* --- nasłuchujemy sygnałów "toggled" --- */ gtk_signal_connect (GTK_OBJECT (elmenu), "toggled", GTK_SIGNAL_FUNC (funkcja), dane); return (elmenu); }
Stan zaznaczalnego elementu menu można ustawiać przy pomocy funkcji gtk_check_menu_item_set_state. /* --- ustawiamy element na niezaznaczony --- */ gtk_check_menu_item_set_state (GTK_CHECK_MENU_ITEM FALSE);
(elmenu),
Opcjonalne elementy menu Elementy menu mogą działać jak przyciski wyboru, ale mogą działać także jak połączone w grupę przyciski opcji (GtkRadioMenuItem). Element menu, który ma zachowywać się jak przycisk opcji, musi być utworzony przy pomocy funkcji gtk_radio_menu_item_new_with_label, przyjmującej nazwę grupy, do której dołączany jest opcjonalny element menu, oraz nazwę tego elementu. Grupa musi zostać ustawiona na NULL przed dodaniem pierwszego elementu menu, a jej nowa wartość musi zostać pobrana po dodaniu każdego elementu. Poniższy kod ilustruje cały proces: GSList *grupa = NULL; /* --- dodajemy do grupy element "Wysoki" --- */ elmenu = gtk_radio_menu_item_new_with_label (grupa, "Wysoki");
Menu, paski narzędziowe i podpowiedzi
85
/* --- uaktualniamy grupę, do której dodaliśmy element --- */ grupa = gtk_radio_menu_item_group (GTK_RADIO_MENU_ITEM (elmenu)); /* --- dodajemy element do menu --- */ gtk_menu_append (GTK_MENU (menu), elmenu); /* --- uwidaczniamy element menu --- */ gtk_widget_show (elmenu); /* --- podłączamy procedurę obsługi zdarzenia --- */ gtk_signal_connect (GTK_OBJECT (elmenu), "toggled", GTK_SIGNAL_FUNC (FunkcjaWysoki), NULL);
GtkRadioMenuItem wywodzi się od GtkCheckMenuItem, więc sygnały są identyczne, podobnie jak funkcje służące do pobierania i ustawiania stanu elementu. Stan elementu GtkRadioMenuItem można zatem ustawić przy pomocy następującej instrukcji: /* --- ustawiamy element na wyłączony --- */ gtk_check_menu_item_set_state (GTK_CHECK_MENU_ITEM FALSE);
(elmenu),
Podpowiedzi Kolejnym elementem interfejsu, którego możemy użyć w aplikacjach, są podpowiedzi (tooltips). Są to niewielkie okienka tekstowe, pojawiające się, kiedy wskaźnik myszy znajdzie się nad kontrolką i znikające, kiedy użytkownik odsunie wskaźnik znad kontrolki. Dostarczają one użytkownikom dodatkowych informacji na temat kontrolki - to znaczy wyjaśniają, do czego służy kontrolka (są przydatne zwłaszcza wtedy, kiedy rysunki na pasku narzędziowym stworzył niezbyt kompetentny grafik). Używa się ich najczęściej w połączeniu z paskami narzędziowymi, ale mogą być także używane wraz z innymi kontrolkami. Podpowiedź tworzymy przy pomocy funkcji gtk_tooltips_new. /* --- tworzymy podpowiedź --- */ podpowiedz = gtk_tooltips_new ();
Po stworzeniu podpowiedzi można skorzystać z funkcji gtk_tooltips_ set_tip, aby dodać je do elementu menu. /* --- dodajemy podpowiedź do elementu menu --- */ gtk_tooltips_set_tip (podpowiedz, elmenu, "Kto rano wstaje, temu Pan Bóg daje", NULL);
86
Część I Programowanie w GTK+
Można zmieniać kolory podpowiedzi przy pomocy funkcji gtk_tooltips_ set_colors. Należy przekazać do niej kolory (GdkColor) tła i tekstu. /* --- zmieniamy kolory --- */ gtk_tooltips_set_colors (podpowiedz, kolor_tla, kolor_tekstu);
Można także ustawić opóźnienie w pojawianiu się podpowiedzi przy pomocy funkcji gtk_tooltips_set_delay. Podpowiedzi można wyłączyć przy pomocy funkcji gtk_tooltips_disable i włączyć przy pomocy gtk_tooltips_ enable.
Klawisze skrótu Wielu ludzi nie lubi wybierania opcji z menu przy pomocy myszy i woli używać skrótów klawiszowych. Dobrzy programiści pamiętają o takich osobach i tworzą skróty, służące do wykonywania najczęściej używanych poleceń. Zanim będzie można używać skrótów, należy je utworzyć i związać z oknem. Funkcja gtk_accel_group_new tworzy nową tablicę GtkAccelGroup, a funkcja gtk_window_add_accelerator_table wiążę tablicę skrótów z oknem. /* --- tworzymy tablicę skrótów --- */ grupa_skrotow = gtk_accel_group_new (); /* --- dodajemy tablicę skrótów do okna aplikacji --- */ gtk_accel_group_attach (grupa_skrotow, GTK_OBJECT (glowne_okno));
Chociaż tablica skrótów jest już utworzona, to klawisze skrótu nie są jeszcze odwzorowane na elementy menu. Funkcja gtk_widget_install_ accelerator odwzorowuje klawisz na element menu. Funkcja ta przyjmuje GtkMenuItem, grupę skrótów, sygnał (zazwyczaj „activate”) oraz klawisze, które będą generować sygnał. Poniższy przykład odwzorowuje sekwencję klawiszy CONTROL+C (GDK_CONTROL_MASK, 'C') na GtkMenuItem i uwidacznia klawisz skrótu w menu (GTK_ACCEL_VISIBLE). Po wciśnięciu sekwencji CONTROL+C zostanie wysłany sygnał „activate”. /* --- menu to jest aktywowane przez klawisze Control+C --- */ gtk_widget_install_accelerator (elmenu, "activate", grupa_skrotow, 'C', GDK_CONTROL_MASK, GTK_ACCEL_VISIBLE);
Menu, paski narzędziowe i podpowiedzi
87
Konsolidacja kodu Można skonsolidować powyższe operacje w jednej funkcji, tworzącej GtkMenuItem wraz ze skrótami i podpowiedziami. Funkcja ta zastępuje 10 do 15 linii kodu dla każdego dodawanego elementu menu i nieco ułatwia pisanie programu. Funkcja przyjmuje parametry dla podpowiedzi i klawisza skrótu, ale jeśli ich nie otrzyma, po prostu ich nie ustawi. Jeśli nie podamy nazwy dla etykiety menu, wówczas zostanie utworzony separator. Cała funkcja wygląda następująco: /* * UtworzElementMenu * * Tworzy element, umieszcza go w menu i zwraca element. * * menu - menu - pojemnik * szNazwa - Nazwa menu - NULL dla separatora * szSkrot - Klawisz skrótu - "^C" dla Control-C * szPodp - Podpowiedzi * funkcja - funkcja zwrotna * dane - dane dla funkcji zwrotnej * * zwraca nowy element menu */ GtkWidget *UtworzElementMenu (GtkWidget *menu, char *szNazwa, char *szSkrot, char *szPodp, GtkSignalFunc funkcja, gpointer dane) { GtkWidget *elmenu; /* --- Jeśli określono nazwę, tworzymy element i przypisujemy * mu procedurę obsługi sygnału */ if (szNazwa && strlen (szNazwa)) { elmenu = gtk_menu_item_new_with_label (szNazwa); gtk_signal_connect (GTK_OBJECT (elmenu), "activate", GTK_SIGNAL_FUNC(funkcja), dane); } else { /* --- Tworzymy separator --- */
Część I Programowanie w GTK+
88 elmenu = gtk_menu_item_new (); }
/* --- Dodajemy element do menu i uwidaczniamy go. --- */ gtk_menu_append (GTK_MENU (menu), elmenu); gtk_widget_show (elmenu); if (grupa_skrotow == NULL) { grupa_skrotow = gtk_accel_group_new (); gtk_accel_group_attach (grupa_skrotow, GTK_OBJECT (glowne_okno)); } /* --- Jeśli określono skrót klawiszowy --- */ if (szSkrot && szSkrot[0] == '^') { gtk_widget_add_accelerator (elmenu, "activate", grupa_skrotow, szSkrot[1], GDK_CONTROL_MASK, GTK_ACCEL_VISIBLE); } /* --- Jeśli określono podpowiedź --- */ if (szPodp && strlen (szPodp)) { gtk_tooltips_set_tip (podpowiedzi, elmenu, szPodp, NULL); } return (elmenu); }
Kiedy mamy już podmenu, możemy wykorzystać powyższą funkcję, aby utworzyć elementy menu. Możemy utworzyć elementy „Nowy” i „Otwórz” wraz z podpowiedziami i klawiszami skrótu w następujący sposób: /* --- tworzymy "Nowy" --- */ elmenu = UtworzElementMenu (menu, "Nowy", "^N", "Tworzy nowy element ", GTK_SIGNAL_FUNC (FunkcjaNowy), "nowy"); elmenu = UtworzElementMenu (menu, "Otwórz", "^O", "Otwiera istniejący element", GTK_SIGNAL_FUNC (FunkcjaOtworz), "otworz");
Menu, paski narzędziowe i podpowiedzi
89
Użycie pojedynczej funkcji ułatwia czytanie kodu. Łatwiej też jest zmienić kod w późniejszym czasie, ponieważ cała procedura tworzenia menu znajduje się w jednym miejscu. Funkcję można rozszerzyć tak, aby odczytywała wszystkie potrzebne jej informacje ze struktury, co pozostawiamy jako ćwiczenie dla Czytelników.
Fabryki elementów (łatwiejsza metoda) Menu można tworzyć w łatwy sposób przy pomocy „fabryk” elementów (GtkItemFactoryEntry). Fabryka elementów korzysta z wstępnie zdefiniowanej struktury, która opisuje tworzone menu. Poszczególne elementy struktury składają się z pięciu części: ścieżki menu, skrótu, funkcji zwrotnej, dodatkowych parametrów dla funkcji zwrotnej oraz znaczników, określających, czym jest dany element. Istnieją następujące znaczniki: . Tworzy element tytułowy. . Tworzy zwykły element menu. . Tworzy zaznaczalny element menu. . Tworzy przełączalny element menu. . Tworzy opcjonalny element menu. . Tworzy separator, rozdzielający elementy menu. . Tworzy element, który będzie przechowywał podmenu. . Tworzy dosunięty do prawej strony element, który będzie przechowywał podmenu. Użycie NULL, 0 albo "" w miejscu znacznika spowoduje utworzenie zwykłego elementu menu. Na przykład podmenu „Nowy” w menu „Plik” mogłoby być zdefiniowane w następujący sposób: "/Plik/Nowy", "N", PlikNowy, NULL, 0
Tekst „Plik/Nowy” jest analizowany w celu zidentyfikowania ścieżki. W tym przypadku pierwszy ukośnik wskazuje menu główne albo pasek menu. „Plik” jest elementem na pasku menu, a „Nowy” jest elementem menu „Plik”. Zapis "N" oznacza, że klawiszem skrótu, który wywołuje to menu jest CTRL+N. Klawisze skrótu mogą wykorzystywać sekwencję ("b"), która oznacza, że wciśnięto klawisz SHIFT, albo sekwencję ("z"), która oznacza, że wciśnięto klawisz ALT. Funkcja zwrotna jest wywoływana wtedy, kiedy użytkownik wybierze element menu przy pomocy myszy albo naciśnie klawisz skrótu. Dodat-
90
Część I Programowanie w GTK+
kowy parametr jest przekazywany do wywoływanej funkcji zwrotnej; ustawienie go na NULL spowoduje przekazanie wartości NULL. Funkcje zwrotne dla fabryki elementów różnią się nieco od standardowych, jeśli chodzi o przekazywane parametry. Otrzymują one w wywołaniu zdefiniowane w tablicy dane, sygnał funkcji zwrotnej i kontrolkę - w takiej kolejności.
Kodowanie fabryk elementów Kiedy tablica GtkItemFactoryEntry zostanie już utworzona, kod wykorzystujący tę tablicę jest dość krótki. Pierwszym krokiem jest utworzenie GtkItemFactory przy pomocy funkcji gtk_item_factory_new. W wywołaniu funkcji podaje się typ menu (GTK_TYPE_MENU_BAR), nazwę oraz grupę skrótów klawiszowych, utworzonych przy pomocy funkcji gtk_accel_ group_new. Elementy menu z tablicy GtkItemFactoryEntry dodaje się przy pomocy funkcji gtk_item_factory_create_items, przekazując jej informacje na temat tablicy. Poniższy przykład korzysta z fabryki elementów, aby stworzyć menu dla okna. Program zawiera funkcję zwrotną, która wyświetla wykonywane czynności, oraz element menu „Zamknij”, który umożliwia wyjście z aplikacji. /* * menu.c * * Menu wykorzystujące fabrykę elementów */ #include static void ZamknijApl (gpointer dane_funkcji, guint sygnal_funkcji, GtkWidget *kontrolka); static void PokazMenu (gpointer dane_funkcji, guint sygnal_funkcji, GtkWidget *kontrolka); /* * Struktura tworzonego menu */ static GtkItemFactoryEntry elem_menu[] = { {"/_Plik", NULL, }, {"/Plik/tearoff1", NULL, },
0,
0,
""
PokazMenu, 0,
""
Menu, paski narzędziowe i podpowiedzi
{"/Plik/_Nowy", {"/Plik/_Otwórz", {"/Plik/_Zapisz", {"/Plik/Zapisz _jako...", {"/Plik/sep1", ""}, {"/Plik/_Zamknij",
91
"N", "O", "S", NULL, NULL,
PokazMenu, 0 }, PokazMenu, 0 }, PokazMenu, 0 }, PokazMenu, 0 }, PokazMenu, 0,
"Q",
ZamknijApl, 0 },
{"/_Edycja",
NULL,
0,
0,
{"/_Edycja/Wytnij", {"/_Edycja/_Kopiuj", {"/_Edycja/_Wklej", {"/_Edycja/_Czcionka",
"X", "C", "V", NULL,
0, 0, 0, 0,
0, 0}, 0, 0}, 0, 0}, 0, ""
""
},
}, {"/_Edycja/Czcionka/Pogrubiona",
NULL, "" }, {"/_Edycja/Czcionka/_Kursywa", NULL, 0, "" }, {"/_Edycja/Czcionka/_Podkreślenie", NULL, PokazMenu, "" }, {"/_Edycja/_Kolor", NULL, 0, 0, {"/_Edycja/Kolor/_Czerwony", NULL, 0, "" }, {"/_Edycja/Kolor/_Niebieski", NULL, 0, "" }, {"/_Edycja/Kolor/_Zielony", NULL, PokazMenu, "" },
PokazMenu,
0,
{"/_Pomoc", "" }, {"/Pomoc/_O programie",
NULL, 0,
PokazMenu, 0, "" }, PokazMenu, PokazMenu, 0,
0,
NULL, PokazMenu, 0 }, }; /* * Zamykamy fabrykę... * * Funkcja zamyka aplikację po wywołaniu jej z menu. */ static void ZamknijApl (gpointer dane_funkcji, guint sygnal_funkcji, GtkWidget *kontrolka) { /* --- Wyświetla komunikat o wybranym elemencie menu --- */ g_message ("Fabryka: aktywowano \"%s\"",
Część I Programowanie w GTK+
92
gtk_item_factory_path_from_widget (kontrolka)); /* --- Zamyka aplikację --- */ gtk_main_quit (); } /* * PokazMenu * * Wyświetla element menu */ static void PokazMenu (gpointer dane_funkcji, guint sygnal_funkcji, GtkWidget *kontrolka) { g_message ("Fabryka: aktywowano \"%s\", czynność %d", gtk_item_factory_path_from_widget (kontrolka), (int) sygnal_funkcji); } /* * --- Liczba elementów w menu */ static int l_elem_menu = sizeof (elem_menu) / sizeof (elem_menu[0]); /* * KoniecApl * * Zamykamy GTK kiedy użytkownik zamyka okno aplikacji */ static gint KoniecApl (GtkWidget *kontrolka, gpointer dane) { gtk_main_quit (); return (TRUE); } /* * */ static void UtworzFabrykeElementow () { GtkWidget *okno = NULL;
Menu, paski narzędziowe i podpowiedzi
GtkWidget *pole1; GtkWidget *pole2; GtkWidget *separator; GtkWidget *etykieta; GtkWidget *przycisk; GtkAccelGroup *grupa_skrotow; GtkItemFactory *fabryka_elem; /* --- tworzymy okno --- */ okno = gtk_window_new (GTK_WINDOW_TOPLEVEL); /* --- ustawiamy minimalny rozmiar --- */ gtk_widget_set_usize (okno, 200, 200); /* --- Podłączamy sygnały w celu usunięcia okna --- */ gtk_signal_connect (GTK_OBJECT (okno), "destroy", GTK_SIGNAL_FUNC (gtk_widget_destroyed), &okno); gtk_signal_connect (GTK_OBJECT (okno), "delete_event", GTK_SIGNAL_FUNC (KoniecApl), NULL); /* --- Tworzymy nową grupę skrótów --- */ grupa_skrotow = gtk_accel_group_new (); /* --- Tworzymy nową fabrykę elementów --- */ fabryka_elem = gtk_item_factory_new (GTK_TYPE_MENU_BAR, "", grupa_skrotow); /* --- tworzymy elementy fabryki na podstawie danych --- */ gtk_item_factory_create_items (fabryka_elem, l_elem_menu, elem_menu, NULL); /* --- Dołączamy grupę skrótów do okna aplikacji --- */ gtk_accel_group_attach (grupa_skrotow, GTK_OBJECT (okno)); /* --- Ustawiamy tytuł okna --- */ gtk_window_set_title (GTK_WINDOW (okno), "Fabryka elementów"); /* --- Bez obramowania --- */ gtk_container_border_width (GTK_CONTAINER (okno), 0); /* --- Pionowe pole pakujące. --- */ pole1 = gtk_vbox_new (FALSE, 0);
93
Część I Programowanie w GTK+
94
gtk_container_add (GTK_CONTAINER (okno), pole1); /* --- Umieszczamy menu w pionowym polu pakującym --- */ gtk_box_pack_start (GTK_BOX (pole1), gtk_item_factory_get_widget (fabryka_elem, ""), FALSE, FALSE, 0); /* --- Uwidaczniamy wszystkie kontrolki --- */ gtk_widget_show_all (okno); } int main (int argc, char *argv[]) { gtk_init (&argc, &argv); UtworzFabrykeElementow (); gtk_main (); return (0); }
Fabryki elementów a menu kodowane ręcznie Fabryki elementów są doskonałym sposobem na szybkie stworzenie menu, ale posiadają pewne ograniczenia, jeśli chodzi o dodawane przez nie elementy. Nie można ich użyć, jeśli chcemy dodać do menu kontrolkę, na przykład piksmapę. Jednak we większości zastosowań są optymalną metodą dodawania menu do aplikacji. Ręczne kodowanie menu jest trudniejsze, ale bardziej elastyczne.
Piksmapy Do wyświetlenia rysunków na przyciskach i innych kontrolkach potrzebne są piksmapy. Są to kontrolki rysunków, umieszczane często jako ikony na przyciskach lub innych kontrolkach i obrazujące działanie danej kontrolki. Paski narzędziowe w aplikacjach składają się często z całego rzędu przycisków graficznych, ponieważ zajmują one mniej miejsca, niż przyciski opisane tekstem. Jednym ze sposobów utworzenia piksmapy jest użycie edytora graficznego (na przykład GIMP-a) i zapisanie rysunku w formacie xpm. Oto przykładowy plik xpm{xe "xpm, pliki"}, utworzony w GIMP-ie (komentarze dodano ręcznie): static const gchar *xpm_otworz[] = { "16 16 4 1", /* bitmapa 16 x 16, 4 kolory, 1 znak / kolor */ " c None", /* kolor 1 - ' ' = kolor przezroczysty */
Menu, paski narzędziowe i podpowiedzi
95
"B c #000000000000", /* kolor 2 - 'B' = czarny */ "Y c #FFFFFFFF0000", /* kolor 3 - 'Y' = żółty */ "y c #999999990000", /* kolor 4 - 'y' = ciemnożółty */ " ", /* tutaj zaczynają się dane rysunku */ " BBB ", " BBBBB B BB ", " BYYYB BB ", " BYYYYYBBBBB ", " BYYYYYYYYYB ", " BYYYYYYYYYB ", " BYYYYYYYYYB ", " BYYBBBBBBBBBBB ", " BYYByyyyyyyyyB ", " BYByyyyyyyyyB ", " BYByyyyyyyyyB ", " BByyyyyyyyyB ", " BByyyyyyyyyB ", " BBBBBBBBBBB ", " ", /* tutaj kończą się dane rysunku */ };
Plik xpm składa się z trzech sekcji: nagłówka, tablicy kolorów i danych bitmapy. Nagłówek jest pierwszym rzędem w tablicy. Wygląda on mniej więcej w ten sposób: "16 16 4 1", gdzie pierwsze dwie liczby to wysokość i szerokość rysunku, trzeci numer to liczba kolorów, zdefiniowanych w tablicy kolorów, a czwarty numer to liczba znaków na kolor. W naszym przykładzie "16 16 4 1" wskazuje, że rysunek ma wysokość 16 pikseli i taką samą szerokość. Trzecia liczba określa liczbę łańcuchów za nagłówkiem, które definiują używane kolory. W przykładzie znajdują się więc cztery linie z danymi kolorów: " c None", "B c #000000000000", "Y c #FFFFFFFF0000", "y c #999999990000",
Każda linia z danymi kolorów definiuje pojedynczy kolor w rysunku. Nagłówek określa, ile znaków (najczęściej jeden) koduje pojedynczy kolor. Każda linia z danymi kolorów zawiera definiowany znak, literę c, oraz szesnastkową wartość albo tekst None, który określa brak koloru (kolor przezroczysty). Wartość koloru określa się w formacie RGB.
Część I Programowanie w GTK+
96
W poprzednim przykładzie, spacje oznaczają brak koloru, znak B oznacza kolor czarny (#000000000000), znak Y kolor żółty (#FFFFFFFF0000), a znak y - ciemnożółty (#999999990000). Wartości szesnastkowe, które reprezentują kolory, są 8-bajtowymi liczbami. Można także użyć czterobajtowych wartości kolorów; ciemnożółty byłby wówczas reprezentowany przez #999900 zamiast #999999990000. Dane rysunku następują bezpośrednio za danymi kolorów. Przy pomocy zdefiniowanych wcześniej kolorów można utworzyć ikonę „Otwórz”, która wygląda jak częściowo otwarty, żółty folder plików. Brak koloru oznaczany jest najczęściej przez spacje, aby łatwiej można było rozpoznać zarysy rysunku w kodzie. Zdefiniowanie rysunku to dopiero początek. Aby go wykorzystać, należy przekształcić go na kontrolkę - będziemy mogli wówczas traktować rysunek tak, jak każdą inną kontrolkę. Aby utworzyć kontrolkę piksmapy, potrzebujemy struktury GdkPixmap, którą należy przekazać do funkcji gtk_pixmap_new wraz z maską. Na szczęście możemy wywołać funkcję GDK gdk_pixmap_create_from_xpm_d, która przyjmuje strukturę danych xpm i zwraca maskę oraz strukturę GdkPixmap. /* * UtworzKontrolkeZXpm * * Konwertuje xpm na kontrolkę piksmapy * Wykorzystuje globalny wskaźnik glowne_okno */ GtkWidget *UtworzKontrolkeZXpm (GtkWidget *okno, gchar **dane_xpm) { GdkBitmap *maska; GdkPixmap *dane_mapy; GtkWidget *kontrolka_piksmapy; /* --- przekształcamy łańcuchy na GdkPixmap --- */ dane_mapy = gdk_pixmap_create_from_xpm_d ( glowne_okno->window, &maska, NULL, (gchar **) dane_xpm); /* --- przekształcamy piksmapę na kontrolkę piksmapy --- */ kontrolka_piksmapy = gtk_pixmap_new (dane_mapy, maska); /* --- uwidaczniamy kontrolkę --- */ gtk_widget_show (kontrolka_piksmapy);
Menu, paski narzędziowe i podpowiedzi
97
/* --- zwracamy kontrolkę --- */ return (kontrolka_piksmapy); }
Kod ten tworzy kontrolkę piksmapy, która będzie widoczna po dodaniu jej do pojemnika. Można więc stworzyć przycisk i dodać do niego piksmapę - otrzymamy wówczas przycisk z rysunkiem. Technika ta umożliwia umieszczanie ikon na przyciskach paska narzędziowego.
Paski narzędziowe Paski narzędziowe udostępniają użytkownikom skróty do najczęściej używanych poleceń. Zawierają zazwyczaj przyciski z ikonami, które reprezentują polecenia, ale mogą też zawierać inne kontrolki. Kontrolkę paska narzędziowego tworzy się przy pomocy funkcji gtk_toolbar_new, która przyjmuje dwa parametry, określające styl paska narzędziowego. Pierwszy parametr określa, czy ikony są rozłożone poziomo (GTK_ORIENTATION_HORIZONTAL), czy pionowo (GTK_ORIENTATION_VERTICAL), natomiast drugi parametr określa, czy przyciski wyświetlają ikony (GTK_TOOLBAR_ICONS), tekst (GTK_TOOLBAR_TEXT), czy też ikony i tekst jednocześnie (GTK_TOOLBAR_BOTH). Pasek narzędziowy w poniższym przykładzie jest ułożony poziomo; ikony leżą w poprzek głównego okna. Dodamy pasek narzędziowy do pionowego pola pakującego, które utworzyliśmy wcześniej. /* --- tworzymy poziomy pasek narzędziowy z ikonami --- */ pasek_narz = gtk_toolbar_new (GTK_ORIENTATION_HORIZONTAL, GTK_TOOLBAR_ICONS); /* --- dodajemy pasek narzędziowy do pionowego pola pakującego --- */ /* --- aplikacji, tuż pod paskiem menu --- */ gtk_box_pack_start (GTK_BOX (ypole), pasek_narz, FALSE, TRUE, 0); /* --- uwidaczniamy pasek narzędziowy --- */ gtk_widget_show (pasek_narz);
Mamy teraz pasek narzędziowy, ale jeszcze bez przycisków. Dodawanie kontrolek do paska i jego formatowanie wymaga użycia jednej z kilku funkcji. Trzema najczęściej używanymi są gtk_toolbar_append_item, gtk_toolbar_append_element i gtk_toolbar_append_space.
98
Część I Programowanie w GTK+
Dodawanie przycisku do paska narzędziowego Funkcja gtk_toolbar_append_item dodaje przycisk do paska narzędziowego. Przyjmuje ona parametry określające piksmapę przycisku, podpowiedź, funkcję zwrotną, i kilka innych danych. Prototyp funkcji wygląda następująco: GtkWidget *gtk_toolbar_append_item ( GtkToolbar *pasek_narz, const char *tekst, const char *tekst_podpowiedzi, const char *prywatny_tekst_podpowiedzi, GtkWidget *kontrolka, GtkSignalFunc *funkcja_zwrotna, gpointer dane_uzytkownika);
pasek_narz to pasek narzędziowy, do którego dodawany jest przycisk. Parametr tekst służy do opisu przycisków tekstowych. Nasz pasek został zdefiniowany jako graficzny, więc pole to zostanie zignorowane. Parametr tekst_podpowiedzi określa podpowiedź, która pojawia się, kiedy użytkownik przesunie wskaźnik myszy ponad przycisk. Parametr prywatny_tekst_podpowiedzi możemy na razie zignorować. kontrolka jest zazwyczaj piksmapą, którą należy umieścić na przycisku. funkcja_zwrotna określa funkcję, wywoływaną po naciśnięciu przycisku, a dane_ uzytkownika to dodatkowe dane, przekazywane do funkcji zwrotnej. Możemy skorzystać z napisanej wcześniej funkcji UtworzKontrolkeZXpm, aby utworzyć kontrolkę z danych xpm i umieścić rysunek na przycisku. Przekazanie piksmapy jako parametru funkcji spowoduje dodanie jej do przycisku. /* --- tworzymy przycisk „Nowy” na pasku narzędziowym --- */ gtk_toolbar_append_item (GTK_TOOLBAR (pasek_narz), NULL, "Nowe okno", NULL, UtworzKontrolkeZXpm (ypole, (gchar **) xpm_nowy), (GtkSignalFunc) PrzyciskZdarzenie, NULL);
Przycisk będzie wyświetlał przekazaną piksmapę jako ikonę. Kiedy użytkownik naciśnie przycisk na pasku narzędziowym, wówczas zostanie wywołana funkcja PrzyciskZdarzenie, w której można umieścić kod odpowiedzialny za przetworzenie zdarzenia. Zazwyczaj wywołuje się tę samą funkcję, którą wywołałoby polecenie z menu („Nowe okno”). Miłą cechą funkcji gtk_toolbar_append_item jest ustawianie procedury obsługi zdarzenia i podpowiedzi, bez potrzeby wywoływania innych funkcji.
Menu, paski narzędziowe i podpowiedzi
99
Kiedy tworzyliśmy menu, poszczególne operacje trzeba było przeprowadzić ręcznie, więc napisaliśmy odpowiedzialną za to funkcję. Tutaj wystarczy pojedyncza funkcja, co ułatwia programowanie pasków narzędziowych.
Dodawanie innych elementów do paska narzędziowego Funkcja gtk_toolbar_append_element przypomina funkcję gtk_toolbar_ append_item, ale jest elastyczniejsza i dlatego bardziej skomplikowana. Oprócz dodawania zwykłych przycisków (GTK_TOOLBAR_ CHILD_BUTTON) umożliwia dodawanie przełączników (GTK_TOOLBAR_CHILD_TOGGLEBUTTON), przycisków opcji (GTK_TOOLBAR_CHILD_RADIOBUTTON), kontrolek (GTK_TOOLBAR_CHILD_WIDGET) oraz tworzenie przerw (GTK_TOOLBAR_CHILD_ SPACE) pomiędzy logicznymi grupami przycisków. Funkcja ta przyjmuje dwa dodatkowe parametry: typ i kontrolkę. Parametr typ określa, jaki rodzaj kontrolki jest wstawiany do paska narzędziowego. Jeśli ustawiony jest na GTK_TOOLBAR_CHILD_WIDGET, wówczas wstawiana jest kontrolka, określona przez parametr kontrolka. GtkWidget *gtk_toolbar_append_element ( GtkToolbarChildType typ, GtkWidget *kontrolka, GtkToolbar *pasek_narz, const char *tekst, const char *tekst_podpowiedzi, const char *prywatny_tekst_podpowiedzi, GtkWidget *ikona, GtkSignalFunc *funkcja_zwrotna, gpointer dane_uzytkownika);
Przy pomocy funkcji gtk_toolbar_append_element możemy dodać na przykład przełącznik: /* --- tworzymy przycisk służący do pogrubiania czcionki --- */ pasek_pogrub = gtk_toolbar_append_element (GTK_TOOLBAR (pasek_narz), GTK_TOOLBAR_CHILD_TOGGLEBUTTON, NULL, NULL, "Pogrubienie", NULL, UtworzKontrolkeZXpm (ypole, (gchar **) xpm_pogrub), (GtkSignalFunc) KliknietoPogrub, NULL);
100
Część I Programowanie w GTK+
Można także najpierw utworzyć GtkToggleButton, przekazać go do funkcji jako parametr kontrolka, i zmienić typ na GTK_TOOLBAR_CHILD_WIDGET. Instrukcja ta informuje GTK+ że przekazujemy własną kontrolkę, więc nie należy tworzyć nowej, jak miało to miejsce w przypadku typu GTK_ TOOLBAR_CHILD_TOGGLEBUTTON. Krótszą wersją tej funkcji jest gtk_toolbar_append_widget. Wymaga ona wstępnego ustawienia większej ilości parametrów, ale ma też większe możliwości. Funkcja jest zdefiniowana następująco: void gtk_toolbar_append_widget (GtkToolbar **pasek_narz, GtkWidget *kontrolka, const gchar *tekst_podpowiedzi, const gchar *prywatny_tekst_podpowiedzi);
Ponieważ funkcja ta nie ustawia funkcji zwrotnych i ikon, trzeba dodać je programowo. Metoda ta zwiększa ilość kodu, ale zapewnia większą elastyczność.
Dodawanie odstępów do paska narzędziowego Funkcja gtk_toolbar_append_space dołącza odstęp na końcu paska narzędziowego. Jedynym potrzebnym parametrem jest pasek, do którego należy dodać odstęp. /* --- dodajemy odstęp pomiędzy kontrolkami --- */ gtk_toolbar_append_space (pasek_narz);
Funkcja gtk_toolbar_append_space wstawia odstęp za ostatnim przyciskiem. Umieszczenie odstępów w pasku narzędziowym umożliwia połączenie przycisków w logiczne grupy. Zgrupowanie przycisków (jedna grupa dla przycisków zmieniających czcionkę, inna dla operacji na plikach) ułatwia użytkownikowi wybór przycisku, wykonującego określone polecenie. Oprócz funkcji dołączających kontrolki na końcu paska narzędziowego, istnieją też funkcje wstawiające je na początek paska (gtk_toolbar_ prepend_item, gtk_toolbar_prepend_element, gtk_toolbar_prepend_space i gtk_t oolbar_prepend_widget). Parametry dla tych funkcji są takie same, jak w przypadku wersji dołączających kontrolki. Czasem zachodzi konieczność wstawienia kontrolki w środek grupy znajdującej się na pasku - zazwyczaj podczas modyfikowania istniejącego paska. Przyciski/kontrolki można wstawiać w dowolnym punkcie paska przy pomocy funkcji _insert_. Przypominają one poprzednie dwie grupy
Menu, paski narzędziowe i podpowiedzi
101
funkcji, ale posiadają dodatkowy parametr, który określa punkt wstawiania przycisku/kontrolki. Jest to liczba całkowita - indeks, wskazujący pozycję na pasku narzędziowym. Ustawienie go na zero wskazuje, że kontrolkę należy dodać jako pierwszy element, a zapis pasek_narz>num_children, że jako ostatni (pasek_narz-> num_children jest aktualną liczbą elementów w pasku narzędziowym).
Tworzenie interfejsu użytkownika dla aplikacji Posiadając wszystkie niezbędne informacje, możemy stworzyć prosty interfejs, który można łatwo zmodyfikować dla dowolnego rodzaju aplikacji. Tworząc aplikację spróbujmy konsolidować kod we wszystkich miejscach, gdzie ma to sens. Jeśli w celu wykonania jakiejś czynności wielokrotnie wywołujemy te same trzy czy cztery funkcje GTK+, warto jest zawrzeć je w pojedynczej funkcji. W rozdziale opisującym menu stworzyliśmy właśnie taką funkcję, w której skonsolidowaliśmy wiele czynności potrzebnych do utworzenia menu. Jesteśmy teraz przygotowani na zbudowanie od podstaw interfejsu użytkownika; podrozdział ten pomoże nam wykonać to zadanie szybko i sprawnie. Potrzebne nam będą dwa menu. W menu „Plik” znajdą się opcje „Nowy”, „Otwórz”, „Zapisz”, „Zapisz jako...”, separator i „Zakończ”. W menu „Edycja” znajdą się opcje „Wytnij”, „Kopiuj”, „Wklej” oraz podmenu „Czcionka”, z opcjami „Pogrubienie”, „Kursywa” i „Podkreślenie”.
Tworzenie okna i menu aplikacji Główny program jest niewielki. Jego jedyne interesujące aspekty to utworzenie podpowiedzi i wywołanie funkcji UtworzGlowneOkno, która wykonuje całą dalszą pracę. /* * main * * --- Tutaj zaczyna się program */ int main(int argc, char *argv[]) { /* --- Inicjujemy GTK --- */ gtk_init (&argc, &argv); /* --- Inicjujemy podpowiedzi --- */
Część I Programowanie w GTK+
102 podpowiedzi = gtk_tooltips_new (); /* --- Tworzymy aplikację --- */ UtworzGlowneOkno (); /* --- Oddajemy sterowanie do GTK --- */ gtk_main(); return 0; }
Funkcja UtworzGlowneOkno zajmuje się utworzeniem większości kontrolek, dokonując tego w kilku krokach. Pierwszym jest utworzenie głównego okna i ustawienie jego atrybutów. Atrybuty takie jak tytuł i rozmiar ustawiane są przed wyświetleniem okna. Inicjujemy tablicę skrótów, którą następnie przypisujemy do głównego okna, aby w menu można było korzystać z klawiszy skrótu, oraz dodajemy procedurę obsługi zdarzenia, która będzie oczekiwać na sygnał "delete_event". Po utworzeniu okna dodajemy do niego pionowe pole pakujące, w którym menu będzie umieszczone na górze, a pozostałe elementy znajdą się pod menu. Po dodaniu pionowego pola pakującego uwidaczniamy główne okno. /* * UtworzGlowneOkno * * Tworzy główne okno i związane z nim menu oraz pasek narzędziowy */ static void UtworzGlowneOkno () { GtkWidget *ypole; GtkWidget *pasekmenu; GtkWidget *menu; GtkWidget *elmenu; GtkWidget *menuczcionka; GtkWidget *pasek_narz; GtkWidget *przycisk; /* --- tworzymy główne okno i ustawiamy jego rozmiary --- */ glowne_okno = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_widget_set_usize(glowne_okno, 360, 260); /* --- ustawiamy tytuł i szerokość obramowania --- */ gtk_window_set_title (GTK_WINDOW (glowne_okno), "Test menu"); gtk_container_border_width (GTK_CONTAINER (glowne_okno), 0);
Menu, paski narzędziowe i podpowiedzi
103
/* --- tworzymy tablicę skrótów klawiszowych --- */ tablica_skrotow = gtk_accelerator_table_new(); gtk_window_add_accelerator_table(GTK_WINDOW(glowne_okno), tablica_skrotow); /* --- Główne okno musi oczekiwać na sygnał delete_event --- */ gtk_signal_connect (GTK_OBJECT (glowne_okno), "delete_event", GTK_SIGNAL_FUNC (EndProgram), NULL); /* --- pole pakujące dla menu i paska narzędziowego --- */ ypole = gtk_vbox_new (FALSE, 0); /* --- umieszczamy pole w oknie --- */ gtk_container_add (GTK_CONTAINER (glowne_okno), ypole); gtk_widget_show (ypole); gtk_widget_show (glowne_okno); /* --- Pasek menu --- */ pasekmenu = gtk_menu_bar_new (); gtk_box_pack_start (GTK_BOX (ypole), pasekmenu, FALSE, TRUE, 0); gtk_widget_show (pasekmenu);
Do paska menu dodajemy podmenu „Plik”, a następnie elementy, które będą znajdować się w menu „Plik”. Są to (w kolejności) „Nowy”, „Otwórz”, „Zapisz”, „Zapisz jako...”, separator i „Zakończ”. Do każdego elementu przypisujemy funkcję obsługi zdarzenia, podpowiedź, a we większości przypadków także klawisz skrótu. Dla większości elementów funkcją zwrotną jest FunkcjaWypisz, która wyświetla na konsoli komunikat, stwierdzający, który element menu został wybrany. Gdyby nasz przykład był rzeczywistą aplikacją, wówczas funkcja zwrotna wykonywałaby jakąś czynność. Ostatnim parametrem funkcji UtworzElementMenu są dane, które przekazywane są do funkcji zwrotnej, dzięki czemu większość elementów menu może używać tej samej funkcji obsługi zdarzenia. /* ----------------* --- Menu Plik --* ----------------- */ menu = UtworzPodmenuPaska (pasekmenu, "Plik"); /* --- Element menu _Nowy_ --- */ elmenu = UtworzElementMenu (menu, "Nowy", "^N", "Tworzy nowy element", GTK_SIGNAL_FUNC (FunkcjaWypisz), "nowy");
Część I Programowanie w GTK+
104
/* --- Element menu _Otwórz_ --- */ elmenu = UtworzElementMenu (menu, "Otwórz", "^O", "Otwiera istniejący element", GTK_SIGNAL_FUNC (FunkcjaWypisz), "otwórz"); /* --- Element menu _Zapisz_ --- */ elmenu = UtworzElementMenu (menu, "Zapisz", "^Z", "Zapisuje bieżący element", GTK_SIGNAL_FUNC (FunkcjaWypisz), "zapisz"); /* --- Element menu _Zapisz jako_ --- */ elmenu = UtworzElementMenu (menu, "Zapisz jako...", "", "Zapisuje bieżący element pod nową nazwą", GTK_SIGNAL_FUNC (FunkcjaWypisz), "zapisz jako"); /* --- separator --- */ elmenu = UtworzElementMenu (menu, NULL, NULL, NULL, NULL, NULL); /* --- _Zakończ_ --- */ elmenu = UtworzElementMenu (menu, "Zakończ", "", "Czy może istnieć bardziej wymowna opcja?", GTK_SIGNAL_FUNC (FunkcjaWypisz), "zakończ");
Menu „Edycja” jest mniejsze i posiada następujące elementy: „Wytnij”, „Kopiuj”, „Wklej” i „Czcionka”. Element „Czcionka” to w istocie podmenu, więc w celu jego stworzenia używamy innej funkcji, niż w przypadku pozostałych elementów. /* -----------------* --- Menu Edycja--* ------------------ */ menu = UtworzPodmenuPaska (pasekmenu, "Edycja"); /* --- Element menu _Wytnij_ --- */ elmenu = UtworzElementMenu (menu, "Wytnij", "^X", "Usuwa element i umieszcza go w schowku", GTK_SIGNAL_FUNC (FunkcjaWypisz), "wytnij"); /* --- Element menu _Kopiuj_ --- */ elmenu = UtworzElementMenu (menu, "Kopiuj", "^C", "Umieszcza kopię elementu w schowku", GTK_SIGNAL_FUNC (FunkcjaWypisz), "kopiuj"); /* --- Element menu _Wklej_ --- */
Menu, paski narzędziowe i podpowiedzi
105
elmenu = UtworzElementMenu (menu, "Wklej", "^V", "Wkleja element", GTK_SIGNAL_FUNC (FunkcjaWypisz), "wklej");
Wreszcie dodajemy elementy menu „Czcionka”: „Pogrubienie”, „Kursywa” i „Podkreślenie”. Będą to zaznaczalne elementy menu, aby mogły pojawiać się w menu w postaci zaznaczonej lub nie zaznaczonej. Pasek narzędziowy tworzymy po utworzeniu wszystkich elementów menu. W poprzednim przykładzie nie zapamiętywaliśmy tworzonych na pasku przycisków, obecnie jednak musimy to uczynić. Powód wyjaśnimy niebawem. /* -----------------------------* --- Podmenu Czcionka --* ---------------------------- */ menuczcionka = UtworzPodmenu (menu, "Czcionka"); menuPogrub = UtworzMenuZazn (menuczcionka, "Pogrubienie", GTK_SIGNAL_FUNC (ZaznaczMenu), "pogrubienie"); menuKurs = UtworzMenuZazn (menuczcionka, "Kursywa", GTK_SIGNAL_FUNC (ZaznaczMenu), "kursywa"); menuPodkr = UtworzMenuZazn (menuczcionka, "Podkreślenie", GTK_SIGNAL_FUNC (ZaznaczMenu), "podkreślenie"); /* --- Tworzymy pasek narzędziowy --- */ UtworzPasek(glowne_ypole);
Zauważmy, że nie mogliśmy skorzystać w tym przykładzie z fabryki elementów, ponieważ w menu znajdują się pola wyboru. Gdyby ich nie było, prawdopodobnie łatwiej byłoby użyć fabryki elementów.
Tworzenie paska narzędziowego Funkcja UtworzPasek tworzy pasek ze wszystkimi ikonami na przyciskach. Dodatkowo umieszcza na nim rozwijaną listę z typami czcionek, aby nadać mu bardziej profesjonalny wygląd. Najpierw trzeba utworzyć pasek i dodać go do pionowego pola pakującego. /* * UtworzPasek * * Tworzy pasek narzędziowy
Część I Programowanie w GTK+
106 */ void UtworzPasek (GtkWidget *ypole) { GtkWidget *kontrolka;
/* --- tworzymy pasek i dodajemy go do okna --- */ pasek_narz = gtk_toolbar_new (GTK_ORIENTATION_HORIZONTAL,
➥GTK_TOOLBAR_ICONS); gtk_box_pack_start (GTK_BOX (ypole), pasek_narz, FALSE, TRUE, 0); gtk_widget_show (pasek_narz); /* --- Tworzymy przycisk "nowy" --- */ gtk_toolbar_append_item (GTK_TOOLBAR (pasek_narz), NULL, "Nowe okno", NULL, UtworzKontrolkeZXpm (ypole, xpm_nowy), (GtkSignalFunc) KliknietoPrzycisk, NULL);
(gchar
**)
/* --- Tworzymy przycisk "otwórz" --- */ gtk_toolbar_append_item (GTK_TOOLBAR (pasek_narz), "Okno dialogowe Otwórz", "Okno dialogowe Otwórz", "", UtworzKontrolkeZXpm (ypole, (gchar **) xpm_otworz), (GtkSignalFunc) KliknietoPrzycisk, NULL); /* --- Mały odstęp --- */ gtk_toolbar_append_space (GTK_TOOLBAR (pasek_narz)); /* --- Przycisk _Wytnij_ --- */ gtk_toolbar_append_item (GTK_TOOLBAR (pasek_narz), "Wytnij", "Wytnij", "", UtworzKontrolkeZXpm (ypole, xpm_wytnij), (GtkSignalFunc) KliknietoPrzycisk, NULL); gtk_toolbar_append_item (GTK_TOOLBAR (pasek_narz), "Wklej", "Wklej", "", UtworzKontrolkeZXpm (ypole, xpm_kopiuj), (GtkSignalFunc) KliknietoPrzycisk,
(gchar
**)
(gchar
**)
Menu, paski narzędziowe i podpowiedzi
107
NULL); /* --- Dodajemy odstęp --- */ gtk_toolbar_append_space (GTK_TOOLBAR (pasek_narz)); /* --- Tworzymy pole kombinowane. Funkcja UtworzPoleCombo * tworzy pole kombinowane i przypisuje do niego funkcję * obsługi zdarzenia. */ kontrolka = UtworzPoleCombo (); /* --- Dodajemy pole kombinowane do paska narzędziowego --- */ gtk_toolbar_append_widget (GTK_TOOLBAR (pasek_narz), kontrolka, "Czcionka", "Wybierz czcionkę"); /* --- mały odstęp --- */ gtk_toolbar_append_space (GTK_TOOLBAR (pasek_narz)); /* * --- Tworzymy przełącznik dla opcji Pogrubienie */ pasek_pogrub = gtk_toolbar_append_element (GTK_TOOLBAR (pasek_narz), GTK_TOOLBAR_CHILD_TOGGLEBUTTON, NULL, NULL, "Pogrubienie", NULL, UtworzKontrolkeZXpm (ypole, (gchar **) xpm_pogrub), (GtkSignalFunc) KliknietoPrzycisk, "pogrubienie"); /* * --- Tworzymy przełącznik dla opcji Kursywa */ pasek_kurs = gtk_toolbar_append_element (GTK_TOOLBAR (pasek_narz), GTK_TOOLBAR_CHILD_TOGGLEBUTTON, NULL, "Kursywa", "Kursywa", "Kursywa", UtworzKontrolkeZXpm (ypole, (gchar **) xpm_kurs), (GtkSignalFunc) KliknietoPrzycisk, "kursywa"); /* * --- Tworzymy przełącznik dla opcji Podkreślenie
Część I Programowanie w GTK+
108 */ pasek_podkr (pasek_narz),
=
gtk_toolbar_append_element
(GTK_TOOLBAR
GTK_TOOLBAR_CHILD_TOGGLEBUTTON, NULL, "Podkreślenie", "Podkreślenie", "Podkreślenie", UtworzKontrolkeZXpm (ypole, (gchar **) xpm_podkr), (GtkSignalFunc) KliknietoPrzycisk, "podkreślenie"); }
Synchronizacja elementów menu i paska narzędziowego Przyciski na pasku narzędziowym i elementy menu są z punktu widzenia GTK+ zupełnie odrębnymi kontrolkami. Nie istnieje między nimi żaden związek, o ile nie stworzymy go w kodzie. Skojarzenie dwóch kontrolek polega zazwyczaj na wyznaczeniu tej samej funkcji zwrotnej dla elementów menu i przycisków paska narzędziowego, co powoduje, że kliknięcie menu lub przycisku daje w aplikacji te same rezultaty. W niektórych programach informacja o statusie pojawia się w kilku miejscach. W tworzonej przez nas aplikacji informacje o parametrach czcionki pojawiają się na pasku narzędziowymi i w zaznaczalnym elemencie menu. Jeśli użytkownik wybierze przycisk „Pogrubienie”, to opcja menu „Pogrubienie” powinna odzwierciedlać zmianę stanu. Jednakże synchronizacja pomiędzy przełącznikiem na pasku i zaznaczalnym menu nie odbywa się automatycznie, ponieważ aplikacja nie posiada żadnej wiedzy na temat związku pomiędzy tymi elementami. Jak uzyskać wymaganą synchronizację? Przypomnijmy sobie, że przyciski paska narzędziowego operujące na czcionce posiadają funkcję zwrotną KliknietoPrzycisk. Funkcja ta wie, który przycisk został wciśnięty i może właściwie ustawić odpowiednik przycisku w menu. Przypomnijmy sobie również, że zachowaliśmy wskaźnik do odpowiedzialnego za wybór czcionki elementu menu, kiedy tworzyliśmy ten element. Możemy sprawdzić stan przełącznika przy pomocy następującego kodu: /* --- Przycisk włączony czy wyłączony? --- */ nStan = GTK_TOGGLE_BUTTON (kontrolka)->active;
Kontrolka jest przekształcana na przełącznik, aby można było sprawdzić jego stan. Możemy napisać niewielką funkcję KliknietoPrzycisk, która odpowiednio ustawi element menu: /*
Menu, paski narzędziowe i podpowiedzi
109
* KliknietoPrzycisk */ void KliknietoPrzycisk (GtkWidget *kontrolka, gpointer dane) { int nStan = GTK_TOGGLE_BUTTON (kontrolka)->active; UstawMenu ((char *) dane, nStan); }
Parametr dane został ustawiony podczas tworzenia kontrolki, aby przekazywała ona do funkcji zwrotnej dane użytkownika. W tym przypadku zmienna dane określa, który przycisk został wciśnięty. Jak pamiętamy, do ustawienia zaznaczalnego elementu menu możemy wykorzystać funkcję gtk_check_menu_item_set_state. Wybrany przycisk przekazuje parametr dane, który zawiera tekst, opisujący wciśnięty przycisk. /* * UstawMenu * * Ustawia zaznaczalny element menu na podstawie nazwy i stanu * * szPrzycisk - nazwa elementu, który należy zmienić * nStan - Stan, w jaki ma znaleźć się element */ void UstawMenu (char *szPrzycisk, int nStan) { GtkCheckMenuItem *elem = NULL; /* --- pokazujemy, w jaki sposób zmieniamy element menu --- */ printf ("check_menu_set_state - %d\n", nStan); /* --- czy to przycisk Pogrubienie? --- */ if (!strcmp (szPrzycisk, "pogrubienie")) { elem = GTK_CHECK_MENU_ITEM(menuPogrub); } else if (!strcmp (szPrzycisk, "kursywa")) { elem = GTK_CHECK_MENU_ITEM(menuKurs); } else if (!strcmp (szPrzycisk, "podkreślenie")) { elem = GTK_CHECK_MENU_ITEM(menuPodkr); } if (elem) { gtk_check_menu_item_set_state (elem, nStan); } }
Część I Programowanie w GTK+
110
Menu musi z kolei ustawiać stan przycisku na pasku narzędziowym. Różnica polega na tym, że do ustawienia stanu przycisku używamy funkcji gtk_toggle_button_set_state, i musimy przekształcić kontrolkę przy pomocy GTK_CHECK_MENU_ITEM, aby sprawdzić, który element menu wybrano. void ZaznaczMenu (GtkWidget *kontrolka, gpointer dane) { GtkToggleButton *przycisk = NULL; char *szPrzycisk; /* --- Sprawdzamy stan elementu menu --- */ int nStan = GTK_CHECK_MENU_ITEM (kontrolka)->active; /* --- pokazujemy parametry i stan przycisku --- */ szPrzycisk = (char *) dane; printf ("wybrane menu %s - %d\n", szPrzycisk, nStan); /* --- przełączamy przycisk na pasku narzędziowym --- */ if (!strcmp (szPrzycisk, "pogrubienie")) { przycisk = GTK_TOGGLE_BUTTON (pasek_pogrub); } else if (!strcmp (szPrzycisk, "kursywa")) { przycisk = GTK_TOGGLE_BUTTON (pasek_kurs); } else if (!strcmp (szPrzycisk, "podkreślenie")) { przycisk = GTK_TOGGLE_BUTTON (pasek_podkr); } if (przycisk) { gtk_toggle_button_set_state (przycisk, nStan); } }
Obie procedury są bardzo podobne. Obecnie po kliknięciu przycisku, odpowiedzialnego za styl czcionki, uaktualniane jest menu, a po kliknięciu menu uaktualniany jest pasek narzędziowy. Dzięki tego rodzaju kosmetyce aplikacja sprawia wrażenie solidnej. Nie ma nic gorszego, niż wyświetlanie odmiennych stanów przez element menu i pasek narzędziowy; no dobrze, są gorsze rzeczy, ale użytkownik aplikacji z pewnością nie byłby tym zachwycony.
Podsumowanie Aplikacje potrzebują dobrego interfejsu użytkownika. Menu, paski narzędziowe, piksmapy i podpowiedzi pomagają w stworzeniu profesjonalnie wyglądającej aplikacji. Istnieje trudniejszy i łatwiejszy sposób two-
Menu, paski narzędziowe i podpowiedzi
111
rzenia menu: sposób trudniejszy jest bardziej elastyczny i pozwala na więcej, na przykład na dodanie do menu pól wyboru. Istnieje wiele funkcji, pomagających w dodawaniu elementów do paska narzędziowego. Piksmapy są rysunkami, które można przekształcić w kontrolki piksmapy i wstawić do przycisku, tworząc tym samym przycisk zaopatrzony w ikonę. W rozdziale napisaliśmy krótkie programy, ilustrujące tworzenie szkieletu aplikacji przy pomocy fabryki elementów lub ręcznego kodowania menu.
Rozdział 6 Dalsze kontrolki: ramki, tekst, okna dialogowe, okno wyboru pliku, pasek postępów Proste kontrolki to dopiero początek GTK+. Pakiet ten udostępnia o wiele więcej kontrolek, pozwalających programistom na tworzenie kompletnych aplikacji. Niniejszy rozdział omawia niektóre spośród nich, w tym kontrolkę ramki, okna dialogowego, paska postępów, oraz dysponującą ogromnymi możliwościami kontrolkę tekstu.
Ramki Ramki są pojemnikami, zazwyczaj otoczonymi obramowaniem i zaopatrzonymi w tytuł, które służą do grupowania kontrolek. Ramki pomagają użytkownikom w rozróżnieniu odrębnych pól na ekranie. Ramkę tworzy się przy pomocy funkcji gtk_frame_new, która tworzy ramkę domyślną. Typowa sekwencja tworzenia ramki wygląda następująco: /* --- tworzymy ramkę --- */ ramka = gtk_frame_new ("Tytuł"); /* --- tutaj ustawiamy jej atrybuty --- */ /* --- uwidaczniamy ramkę --- */ gtk_widget_show (ramka); /* --- dodajemy ją do pojemnika albo pola --- */ gtk_container_add (okno, ramka);
Po stworzeniu ramki możemy wykorzystać funkcję gtk_frame_set_label, aby zmienić etykietę ramki: gtk_frame_set_label (ramka, "Hej, nowy tytuł!");
Część I Programowanie w GTK+
114
Właściwości brzegu ramki można ustawić przy pomocy funkcji gtk_frame_set_shadow. Właściwości ramki określają, w jaki sposób ramka będzie wyglądała na ekranie. Ustawienie ramki na GTK_SHADOW_IN sprawia wrażenie, że cały obszar ramki jest wklęsły, natomiast GTK_SHADOW_OUT sprawia wrażenie, że ramka jest wyniesiona ponad inne kontrolki. Efekt ten osiąga się przy pomocy odpowiedniego doboru kolorów. Częściej używane wartości GTK_SHADOW_ETCHED_OUT i GTK_SHADOW_ETCHED_IN rysują tylko brzeg ramki, a GTK_SHADOW_ ETCHED_NONE w ogóle nie rysuje brzegu. Różne typy ramek przedstawia rysunek 6.1.
Rysunek 6.1. Ramki.
Tytuł może pojawić się w ramce w środku, po lewej albo po prawej stronie. Do zmiany justowania służy funkcja gtk_frame_set_label_align. Parametry, których używaliśmy do wyrównywania tekstu etykiet, mają zastosowanie także tutaj. Wyrównanie do lewej określamy przez 0, do środka przez .5, a do prawej przez 1. Poniższe przykłady ilustrują użycie parametrów: /* --- ustawiamy etykietę ramki po lewej stronie --- */ gtk_frame_set_label_align (GTK_FRAME (ramka), 0, 0); /* --- ustawiamy etykietę ramki w środku --- */ gtk_frame_set_label_align (GTK_FRAME (ramka), .5, 0); /* --- ustawiamy etykietę ramki po prawej stronie --- */ gtk_frame_set_label_align (GTK_FRAME (ramka), 1, 0);
Dalsze kontrolki: ramki, tekst, okna dialogowe, okno wyboru pliku, pasek postępów
115
Po utworzeniu ramki można dodawać do niej kontrolki. Ramkę należy traktować jako zwykła kontrolkę pojemnika, choć zaopatrzoną w obramowanie i tytuł. Jeśli chcemy dodać do niej więcej niż jedną kontrolkę, musimy użyć pola albo tabeli pakującej.
Kontrolka tekstu Kontrolka tekstu (patrz rysunek 6.2) przypomina kontrolkę wpisu (patrz rozdział 4), ale umożliwia wprowadzenie wielu linii tekstu, co predestynuje ją do wykorzystania w edytorze; zajmiemy się tym zresztą w dalszym rozdziale. Kontrolka tekstu jest znacznie bardziej skomplikowana, niż mniejsze kontrolki, ponieważ posiada dużo więcej ustawialnych atrybutów. Kontrolka tekstu obsługuje pewną liczbę skrótów klawiszowych, w tym standardowe skróty służące do wycinania, kopiowania i wklejania tekstu. Skróty te można stosować również w opisanej wcześniej kontrolce wpisu, ponieważ obie wywodzą się od GtkEditable. Oto niektóre skróty klawiszowe: CTRL+A - Początek linii CTRL+E - Koniec linii CTRL+N - Następna linia CTRL+P - Poprzednia linia CTRL+B - Jeden znak do tyłu CTRL+F - Jeden znak do przodu ALT+B - Jedno słowo do tyłu ALT+F - Jedno słowo do przodu Skróty edycyjne: CTRL+H - Usuwa znak przed kursorem (Backspace) CTRL+D - Usuwa znak za kursorem (Delete) CTRL+W - Usuwa słowo przed kursorem ALT+D - Usuwa słowo za kursorem CTRL+K - Usuwa znaki do końca linii CTRL+U - Usuwa linię Skróty operujące na zaznaczeniach:
Część I Programowanie w GTK+
116
CTRL+X - Wycina tekst i umieszcza go w schowku CTRL+C - Kopiuje tekst do schowka CTRL+V - Wkleja tekst ze schowka Kontrolka tekstu jest niemal gotowym edytorem. Stworzenie przy jej pomocy pełnej aplikacji edytora nie wymaga wiele pracy, o czym można przekonać się w rozdziale 8, „Tworzenie prostego edytora tekstu”.
Rysunek 6.2. Kontrolka tekstu.
Tworzenie kontrolki tekstu Kontrolkę tekstu tworzymy przy pomocy funkcji gtk_text_new, przekazując do niej wartości typu GtkAdjustment, która określają parametry poziomej i pionowej regulacji. Zazwyczaj można tu użyć NULL, co spowoduje zastosowanie domyślnej regulacji. Kontrolkę tekstu tworzymy zatem w następujący sposób: tekst = gtk_text_new (NULL, NULL);
Kontrolkę tekstu można ustawić w tryb „tylko do odczytu” przy pomocy funkcji gtk_text_set_editable. Funkcja ta ustawia pole na „tylko do odczytu”, jeśli przekażemy jej parametr FALSE; w innym przypadku zawartość kontrolki będzie można edytować. Domyślnie edycja jest dozwolona. /* --- pozwalamy na edytowanie kontrolki tekstu --- */ gtk_text_set_editable (GTK_TEXT (tekst), TRUE); /* --- przełączamy kontrolkę w tryb "tylko do odczytu" --- */ gtk_text_set_editable (GTK_TEXT (tekst), FALSE);
Można ustawić pozycję kursora przy pomocy funkcji gtk_text_set_point. Funkcja ta przyjmuje parametr w postaci indeksu, który określa, gdzie
Dalsze kontrolki: ramki, tekst, okna dialogowe, okno wyboru pliku, pasek postępów
117
ma znaleźć się kursor. Indeks oznacza numer znaku w tekście, na przykład: /* --- ustawiamy kursor za dziesiątym znakiem --- */ gtk_text_set_point (GTK_TEXT (tekst), 10);
Można także sprawdzić pozycję kursora przy pomocy funkcji gtk_text_get_point. Zwraca ona indeks, który określa przesunięcie kursora względem początku tekstu. /* --- sprawdzamy, gdzie jest kursor --- */ gtk_text_get_point (GTK_TEXT (tekst));
Funkcja gtk_text_get_length służy do sprawdzania, ile znaków znajduje się w kontrolce tekstu, natomiast funkcja gtk_editable_get_chars służy do pobierania tekstu, znajdującego się w kontrolce. Na przykład, aby pobrać wszystkie znaki w kontrolce tekstu, można użyć następującego kodu: /* --- pobieramy liczbę znaków w kontrolce --- */ nDlugosc = gtk_text_get_length (GTK_TEXT (tekst); /* --- pobieramy tekst z kontrolki --- */ bufor = gtk_editable_get_chars (GTK_EDITABLE (tekst), (gint) 0, (gint) nDlugosc);
Zamiast obliczać długość tekstu w kontrolce, z wartości -1, która wskazuje na ostatni znak.
można
skorzystać
/* --- pobieramy tekst z kontrolki --- */ bufor = gtk_editable_get_chars (GTK_EDITABLE (tekst), (gint) 0, (gint) -1);
Bufor zwracany przez funkcję gtk_editable_get_chars jest nowym blokiem pamięci, przydzielanym specjalnie dla wartości powrotnej. Zajmowanie tej pamięci aż do momentu zakończenia aplikacji sprzeciwia się regułom sztuki programowania (ktoś mógłby wówczas pomyśleć, że jest to aplikacja Microsoftu!). Należy więc użyć funkcji g_free, kiedy pamięć nie jest już potrzebna. /* --- pamięć jest już niepotrzebna, zwalniamy ją --- */ g_free (bufor);
Można pobrać pojedynczy znak z kontrolki tekstu przy pomocy makra GTK_TEXT_INDEX. Przekazujemy mu indeks i otrzymujemy znak z określonej pozycji.
118
Część I Programowanie w GTK+
/* --- pobieramy sto pierwszy znak z kontrolki --- */ znak = GTK_TEXT_INDEX (GTK_TEXT (tekst), 101);
Można także wstawić tekst do kontrolki przy pomocy funkcji gtk_text_insert_text. Wstawia ona znaki w bieżącej pozycji kursora. Jeśli tekst ma być wstawiony przy użyciu określonej czcionki lub koloru, należy użyć funkcji gtk_text_insert wraz z informacjami o kolorze i czcionce. Przekazanie NULL spowoduje użycie bieżących parametrów tekstu. /* --- dodajemy tekst bez określania czcionki/koloru --- */ gtk_text_insert (GTK_TEXT (tekst), NULL, NULL, NULL, bufor, strlen (bufor)); /* --- dodajemy tekst z określoną czcionką/kolorem --- */ gtk_text_insert (GTK_TEXT (tekst), czcionka, kolor_tekstu, kolor_tla, bufor, strlen (bufor)); /* --- dodajemy tekst z bieżącą czcionką/kolorem --- */ gtk_text_insert_text (GTK_TEXT (tekst), bufor, strlen (bufor), &nPozycja);
Usuwanie tekstu odbywa się względem bieżącej pozycji kursora. Usuwamy tekst zaczynając od bieżącej pozycji kursora, a kończąc na określonym punkcie przed lub za kursorem. Funkcja gtk_text_forward_delete usuwa znaki znajdujące się za kursorem, a funkcja gtk_text_backward_ delete przed kursorem. /* --- usuwamy dziesięć znaków za kursorem --- */ bSukces = gtk_text_forward_delete (GTK_TEXT (tekst), (guint) 10); /* --- usuwamy dziesięć znaków przed kursorem --- */ bSukces = gtk_text_backward_delete (GTK_TEXT (tekst), (guint) 10);
Zwracaną wartością jest TRUE, jeśli usunięcie znaków się powiodło, a FALSE w przeciwnym przypadku.
Wstawianie i usuwanie tekstu Wstawianie i usuwanie tekstu może być wolne, jeśli operujemy na dużych ilościach tekstu. Kontrolka jest przerysowywana po każdym wstawieniu lub usunięciu tekstu, co nie zawsze jest pożądane, zwłaszcza jeśli naraz wstawiamy lub usuwamy kilka łańcuchów. Czasem lepszym rozwiązaniem jest wstrzymanie rysowania kontrolki na czas operacji wsta-
Dalsze kontrolki: ramki, tekst, okna dialogowe, okno wyboru pliku, pasek postępów
119
wiania i usuwania tekstu. Można wstrzymać automatyczne uaktualnianie obrazu, wywołując funkcję gtk_text_freeze, i ponownie je uruchomić funkcją gtk_text_thaw. Funkcję gtk_text_freeze wywołujemy przed modyfikacją tekstu w kontrolce. Kiedy dokonamy żądanych modyfikacji, używamy funkcji gtk_text_thaw aby „odmrozić” kontrolkę. Spowoduje to jej przerysowanie, o ile dokonano zmian, kiedy była zamrożona. Odpowiedni kod wygląda następująco: /* --- Zamrażamy kontrolkę --- */ gtk_text_freeze (GTK_TEXT (tekst)); /* --- ...tutaj modyfikujemy tekst kontrolki... --- */ /* --- Odmrażamy kontrolkę --- */ gtk_text_thaw (GTK_TEXT (tekst));
Jeśli nie użyjemy funkcji gtk_text_freeze i gtk_text_thaw podczas wstawiania lub usuwania dużej ilości tekstu, kontrolka będzie przerysowywana po każdej operacji. Zamiast sekwencji [wstaw tekst][wstaw tekst][wstaw tekst][przerysuj] aplikacja wykona więc sekwencję [wstaw tekst][przerysuj] [wstaw tekst][przerysuj][wstaw tekst][przerysuj]. Dwa dodatkowe przerysowania są niepotrzebne, ponieważ kontrolka i tak zostanie poddana dalszym modyfikacjom, które będą wymagały jej uaktualnienia. Lepiej jest więc poinformować kontrolkę, aby zaniechała aktualizacji, zanim nie dobiegną końca wszystkie modyfikacje. Sytuacja taka może mieć miejsce na przykład podczas operacji szukania i zamiany w edytorze. Korzystniej jest zamrozić ekran podczas dokonywania zamian i wyświetlić dopiero ostateczny rezultat, niż spowalniać całą operację przez wielokrotne odświeżanie kontrolki po każdej zmianie tekstu. Kontrolki tekstu używane w różnego rodzaju edytorach muszą obsługiwać opcje wycinania, kopiowania i wklejania. Kontrolka tekstu automatycznie obsługuje wycinanie (CTRL+X), kopiowanie (CTRL+C) i wklejanie (CTRL+V). Wiele osób lubi jednak podczas kopiowania czy wklejania korzystać z myszy; możemy zaimplementować te operacje przy pomocy odpowiednich funkcji. Funkcja gtk_editable_copy_clipboard kopiuje zaznaczony tekst i umieszcza go w schowku. Tekst pozostaje tam, dopóki nie zostanie przywołany lub nadpisany. /* --- kopiujemy zaznaczony tekst i umieszczamy go w schowku --- */ gtk_editable_copy_clipboard (GTK_EDITABLE GDK_CURRENT_TIME);
(tekst),
120
Część I Programowanie w GTK+
Funkcja gtk_editable_cut_clipboard usuwa zaznaczony tekst z kontrolki i przenosi go do schowka. /* --- usuwamy zaznaczony tekst i umieszczamy go w schowku --- */ gtk_editable_cut_clipboard (GTK_EDITABLE GDK_CURRENT_TIME);
(tekst),
Funkcja gtk_editable_paste_clipboard pobiera tekst ze schowka i wstawia go w bieżącej pozycji kursora. /* --- kopiujemy tekst ze schowka i wstawiamy go do kontrolki --- */ /* --- w bieżącej pozycji kursora --- */ gtk_editable_paste_clipboard (GTK_EDITABLE GDK_CURRENT_TIME);
(tekst),
Paski przewijania Paski przewijania są istotnym elementem kontrolki tekstu. Przydają się w sytuacji, kiedy użytkownik wprowadzi więcej tekstu, niż może zmieścić się na ekranie, co - o dziwo - zdarza się bardzo często. Kontrolka może pomieścić więcej tekstu, niż jest w stanie wyświetlić. Użytkownik może zechcieć przewinąć dane przy pomocy myszy, ale bez pasków przewijania raczej nie będzie to możliwe. Kontrolka tekstu będzie znacznie użyteczniejsza, jeśli wyposażymy ją w paski przewijania, które pozwolą użytkownikowi przeglądać tekst przy pomocy myszy. Tworzenie pasków przewijania i dołączanie ich do kontrolki tekstu jest bardzo łatwe. Poniższy kod tworzy kontrolkę tekstu wewnątrz tabeli i dodaje paski przewijania do pola edycyjnego. Tworzymy tabelę pakującą 2 x 2, aby umieścić w niej kontrolkę tekstu z paskiem przewijania pionowego po jej prawej stronie, a paskiem przewijania poziomego na spodzie. Kontrolka tekstu znajduje się w lewym górnym rogu (0-1, 0-1) tabeli pakującej, pasek pionowy w prawym górnym rogu (1-2, 0-1), a pasek poziomy w lewym dolnym rogu (0-1, 1-2). /* --- tworzymy kontrolkę --- */ tabela = gtk_table_new (2, 2, FALSE); /* --- uwidaczniamy ją --- */ gtk_widget_show (tabela); /* --- tworzymy kontrolkę --- */ tekst = gtk_text_new (NULL, NULL); /* --- dodajemy kontrolkę do tabeli pakującej --- */ gtk_table_attach (GTK_TABLE (tabela), tekst, 0, 1, 0, 1,
Dalsze kontrolki: ramki, tekst, okna dialogowe, okno wyboru pliku, pasek postępów
121
GTK_EXPAND | GTK_SHRINK | GTK_FILL, GTK_EXPAND | GTK_SHRINK | GTK_FILL, 0, 0); /* --- uwidaczniamy ją --- */ gtk_widget_show (tekst);
Po umieszczeniu kontrolki tekstu w tabeli pakującej możemy dodać do niej paski przewijania. Powinny one zostać dodane do struktury, opisującej kontrolkę tekstu. W strukturze tej znajdują się pola hadj i vadj, które są wskaźnikami typu GtkAdjustment. Aby utworzyć pasek przewijania i związać z nim wskaźnik GtkAdjustment kontrolki tekstu, należy po prostu przekazać do niego ten wskaźnik w momencie tworzenia. /* --------------------------------------------------- */ /* --- tworzymy poziomy pasek przewijania --- */ /* --------------------------------------------------- */ xpasek = gtk_hscrollbar_new{xe " gtk_hscrollbar_new, funkcja"}{xe "funkcje: gtk_hscrollbar_new "} (GTK_TEXT (tekst)->hadj); /* --- umieszczamy poziomy pasek pod kontrolką tekstu --- */ gtk_table_attach (GTK_TABLE (tabela), xpasek, 0, 1, 1, 2, GTK_EXPAND | GTK_SHRINK | GTK_FILL, GTK_FILL, 0, 0); /* --- Wyświetlamy kontrolkę --- */ gtk_widget_show (xpasek); /* --------------------------------------------------- */ /* --- tworzymy pionowy pasek przewijania --- */ /* --------------------------------------------------- */ ypasek = gtk_vscrollbar_new{xe " gtk_vscrollbar_new, funkcja"}{xe "funkcje: gtk_vscrollbar_new "} (GTK_TEXT (tekst)->vadj); /* --- umieszczamy pionowy pasek prawej stronie tekstu --- */ gtk_table_attach (GTK_TABLE (tabela), ypasek, 1, 2, 0, 1, GTK_FILL, GTK_EXPAND | GTK_SHRINK | GTK_FILL, 0, 0); /* --- Wyświetlamy kontrolkę --- */ gtk_widget_show (ypasek);
Jeśli poziomy pasek przewijania nie jest potrzebny, możemy po prostu utworzyć pionowe pole pakujące i umieścić pionowy pasek przewijania obok kontrolki tekstu. Ilustruje to następujący kod: /* --- tworzymy kontrolkę --- */ tekst = gtk_text_new (NULL, NULL);
122
Część I Programowanie w GTK+
/* --- dodajemy kontrolkę tekstową do pola pakującego --- */ gtk_box_pack_start (GTK_BOX (ypole), tekst, FALSE, FALSE, 0); /* --- tworzymy pionowy pasek przewijania --- */ ypasek = gtk_vscrollbar_new (GTK_TEXT (tekst)->vadj); /* --- dodajemy pasek po prawej stronie kontrolki tekstu --- */ gtk_box_pack_start (GTK_BOX (ypole), ypasek, FALSE, FALSE, 0); /* --- uwidaczniamy pasek --- */ gtk_widget_show (ypasek);
Okna dialogowe Okna dialogowe służą do wyświetlania informacji („Twój dysk jest bliski zapełnienia”), zadawania pytań użytkownikowi („Czy na pewno chcesz to zrobić?”) albo uzyskiwania innych informacji od użytkownika („Pod jaką nazwą chcesz zapisać plik?”). Okna dialogowe tworzy się przy użyciu funkcji gtk_dialog_new, która tworzy puste okno dialogowe, bez przycisków i tekstu. Utworzone okno dialogowe posiada dwa pola pakujące (vbox, action_area), do których można dodawać inne kontrolki. Pole vbox służy do umieszczania etykiet albo innych informacji, a przyciski umieszcza się w action_area. Puste okno dialogowe nie przyda się do niczego, dopóki nie umieścimy w nim kontrolki tekstu i jednego lub dwóch przycisków. Musi też wyświetlać jakąś informację dla użytkownika. Najłatwiejszym sposobem uczynienia okna użytecznym jest wyświetlenie jakiejś informacji i przycisku „OK”. Możemy utworzyć etykietę i dodać ją do pionowego pola pakującego vbox w następujący sposób: /* --- tworzymy etykietę --- */ etykieta = gtk_label_new (szKomunikat); /* --- dajemy jej trochę przestrzeni życiowej --- */ gtk_misc_set_padding (GTK_MISC (etykieta), 10, 10); /* --- dodajemy ją to pionowego pola pakującego --- */ gtk_box_pack_start (GTK_BOX (GTK_DIALOG (okno_dialogowe)->vbox), etykieta, TRUE, TRUE, 0); /* --- uwidaczniamy etykietę --- */ gtk_widget_show (etykieta);
Dalsze kontrolki: ramki, tekst, okna dialogowe, okno wyboru pliku, pasek postępów
123
Okno wygląda miło, ale nadal nie dostarcza żadnych informacji od użytkownika. Musimy więc dodać przycisk do obszaru action_area. Kiedy dodajemy przycisk do okna dialogowego, zazwyczaj dobrze jest ustawić jego znacznik GTK_CAN_DEFAULT{xe "GTK_CAN_DEFAULT"}. Znacznik ten określa, że przycisk może być domyślną kontrolką w oknie dialogowym, to znaczy użytkownik może nacisnąć ENTER zamiast klikać na przycisku. Funkcja gtk_widget_ grab_default umożliwia ustawianie domyślnej kontrolki. W naszym prostym przykładzie mamy tylko jeden przycisk; możemy więc go uczynić domyślnym przyciskiem dla okna dialogowego, aby użytkownik mógł wybrać go przy pomocy klawisza ENTER. Domyślny przycisk posiada szerokie obramowanie. Dodanie przycisku „OK” jest nadzwyczaj proste: /* --- tworzymy przycisk --- */ przycisk = gtk_button_new_with_label ("OK"); /* --- pozwalamy na uczynienie przycisku domyślnym --- */ GTK_WIDGET_SET_FLAGS (przycisk, GTK_CAN_DEFAULT); /* --- dodajemy przycisk do obszaru action_area --- */ gtk_box_pack_start (GTK_BOX(GTK_DIALOG >action_area), przycisk, TRUE, TRUE, 0);
(okno_dialogowe)-
/* --- przesuwamy ognisko na przycisk --- */ gtk_widget_grab_default (przycisk); /* --- uwidaczniamy go --- */ gtk_widget_show (przycisk); /* --- oczekujemy na kliknięcie przycisku --- */ gtk_signal_connect (GTK_OBJECT (przycisk), "clicked", GTK_SIGNAL_FUNC (kliknietoOK), &wartosc_zwrotna);
Linux obsługuje kilka typów okien dialogowych. Niektóre z nich są oknami modalnymi, inne niemodalnymi. Okna modalne ogniskują sterowanie w aplikacji na oknie dialogowym tak, że użytkownik nie może używać innych okien, dopóki nie zamknie okna dialogowego. Program może przetwarzać tylko te zdarzenia, które pochodzą z okna dialogowego. Aplikacja w Linuksie może wyświetlić modalne okno dialogowe aby upewnić się, że użytkownik zapoznał się z jakąś informacją, bądź też wtedy, kiedy informacje uzyskiwane z okna są niezbędne do dalszej pracy programu. Często modalne jest okno wyboru pliku - ponieważ użytkownik chce wczytać plik, nie ma sensu robić niczego innego, dopóki
124
Część I Programowanie w GTK+
użytkownik nie wybierze pliku i nie naciśnie „OK”, albo nie zrezygnuje z wczytywania pliku, naciskając „Anuluj”. Kilka okien dialogowych w działaniu przedstawia rysunek 6.3. Niemodalne okna dialogowe nie posiadają takich właściwości. Można na przykład wyświetlić w oknie dialogowym tablicę kolorów, która pozwala użytkownikowi na wybór koloru. W tym samym czasie mogą być aktywne inne okna, bez potrzeby ograniczania ogniska do okna wyboru koloru. Zamieszczona poniżej funkcja OknoDialogowe tworzy proste okno dialogowe i czeka, aż użytkownik wciśnie przycisk „OK”. Tworzymy okno dialogowe, etykietę dla wyświetlanego komunikatu (umieszczając ją w oknie dialogowym), wreszcie tworzymy przycisk „OK” i również umieszczamy go w oknie. Przycisk posiada przypisaną funkcję obsługi zdarzenia, która zamknie okno po naciśnięciu przycisku. Okno to jest modalne, ponieważ wywołuje funkcję gtk_grab_add, która zapewnia, że zdarzenia mogą zachodzić tylko w tym oknie.
Rysunek 6.3. Okna dialogowe. /* * OknoDialogowe * * Wyświetla okno dialogowe z komunikatem i przyciskiem "OK" */ void OknoDialogowe (char *szKomunikat) { static GtkWidget *etykieta; GtkWidget *przycisk;
Dalsze kontrolki: ramki, tekst, okna dialogowe, okno wyboru pliku, pasek postępów
125
GtkWidget *okno_dialogowe; /* --- tworzymy okno dialogowe --- */ okno_dialogowe = gtk_dialog_new (); /* --- przechwytujemy sygnał zamykania okna, --- */ /* --- aby zwolnić ognisko --- */ gtk_signal_connect (GTK_OBJECT (okno_dialogowe), "destroy", GTK_SIGNAL_FUNC (ZamkniecieOkna), &okno_dialogowe); /* --- dodajemy tytuł do okna --- */ gtk_window_set_title (GTK_WINDOW dialogowe");
(okno_dialogowe),
"Okno
/* --- tworzymy niewielkie obramowanie --- */ gtk_container_border_width (GTK_CONTAINER (ono_dialogowe), 5); /* -------------------------------- */ /* --- tworzymy komunikat --- */ /* -------------------------------- */ /* --- umieszczamy komunikat w etykiecie --- */ etykieta = gtk_label_new (szKomunikat); /* --- zostawiamy trochę miejsca wokół etykiety --- */ gtk_misc_set_padding (GTK_MISC (etykieta), 10, 10); /* --- dodajemy etykietę do okna dialogowego --- */ gtk_box_pack_start (GTK_BOX (GTK_DIALOG (okno_dialogowe)->vbox), etykieta, TRUE, TRUE, 0); /* --- uwidaczniamy etykietę --- */ gtk_widget_show (etykieta); /* ----------------------- */ /* --- przycisk "OK" --- */ /* ----------------------- */ /* --- tworzymy przycisk "OK" --- */ przycisk = gtk_button_new_with_label ("OK"); /* --- musimy zamknąć okno po naciśnięciu "OK" --- */ gtk_signal_connect (GTK_OBJECT (przycisk), "clicked", GTK_SIGNAL_FUNC (ZamknijDialog),
Część I Programowanie w GTK+
126 okno_dialogowe);
/* --- pozwalamy, aby przycisk mógł być domyślnym --- */ GTK_WIDGET_SET_FLAGS (przycisk, GTK_CAN_DEFAULT); /* --- dodajemy przycisk do okna dialogowego --- */ gtk_box_pack_start (GTK_BOX ( GTK_DIALOG (okno_dialogowe)->action_area), przycisk, TRUE, TRUE, 0); /* --- czynimy przycisk domyślnym --- */ gtk_widget_grab_default (przycisk); /* --- uwidaczniamy przycisk --- */ gtk_widget_show (przycisk); /* --- uwidaczniamy okno dialogowe --- */ gtk_widget_show (okno_dialogowe); gtk_grab_add (okno_dialogowe); }
Funkcja ZamknijDialog, przypisana do przycisku OK, wywołuje funkcję gtk_grab_remove, aby zakończyć tryb modalny okna dialogowego, a następnie zamyka okno. Do funkcji przekazywany jest w polu danych wskaźnik GtkWidget okna dialogowego, ponieważ funkcja zwrotna nie wie, z którego okna dialogowego pochodzi sygnał (wie tylko, że sygnał pochodzi od jakiejś kontrolki). Przekazywanie okna dialogowego do funkcji zwrotnej ustawiliśmy w wywołaniu gtk_signal_connect; dzięki temu będziemy mogli zamknąć okno. /* * ZamknijOkno * * Zamyka okno dialogowe. Otrzymuje jako parametr uchwyt do okna. */ void ZamknijOkno (GtkWidget *kontrolka, gpointer dane) { /* --- zamykamy okno --- */ gtk_widget_destroy (GTK_WIDGET (dane)); }
Podczas tworzenia okna dialogowego użyliśmy funkcji gtk_grab_add, aby uczynić je modalnym. Zamykając okno należy wywołać funkcję gtk_grab_
Dalsze kontrolki: ramki, tekst, okna dialogowe, okno wyboru pliku, pasek postępów
127
remove, aby usunąć modalność okna. Sygnał "destroy" nadaje się do tego idealnie, ponieważ wskazuje, że okno jest zamykane. /* * ZamkniecieOkna * * Wywoływana przed zamknięciem okna. Zwraca FALSE, aby pozwolić * na jego zamknięcie. Zwalnia przechwycenie sygnałów, które uczyniło * okno modalnym. */ void ZamkniecieOkna (GtkWidget *kontrolka, gpointer dane) { gtk_grab_remove (GTK_WIDGET (kontrolka)); {
Przykłady okien dialogowych „Tak/Nie” albo „OK/Anuluj” są nieco bardziej skomplikowane. Wyświetlają one komunikat i umożliwiają użytkownikowi dokonanie wyboru. Zadają pytania typu „Czy na pewno chcesz to zrobić?”, a wybór któregoś z przycisków powoduje odmienne reakcje programu. Możemy tworzyć okna dialogowe tego typu rozszerzając poprzedni przykład, poprzez przypisanie każdemu przyciskowi w oknie dialogowym innej procedury obsługi. Zauważmy, że w kolejnym przykładzie zarówno dla przycisku „Tak”, jak i „Nie” ustawiony jest znacznik GTK_CAN_DEFAULT. Dzięki temu obydwa przyciski mogą stać się domyślnymi. Jeśli klikniemy na dowolnym z nich i przytrzymamy przycisk myszy, wówczas zostanie on otoczony szerokim obramowaniem. Naciśnięcie w tym momencie klawisza ENTER jest równoznaczne z naciśnięciem tego przycisku. Okno dialogowe jest początkowo wyświetlane z domyślnym przyciskiem „Nie”, co określamy w wywołaniu funkcji gtk_widget_grab_default. Zazwyczaj jako domyślne powinno się ustawiać działanie niedestrukcyjne; na przykład program, który formatuje dysk twardy i zadaje pytanie „Czy na pewno chcesz to zrobić?”, powinien prawdopodobnie ustawić przycisk „Nie” jako domyślny. Funkcja TakNie przyjmuje komunikat („Czy na pewno chcesz to zrobić?”) i dwie funkcje. Funkcja FunkcjaTak powinna być wywoływana wtedy, kiedy użytkownik naciśnie przycisk „Tak”, a FunkcjaNie w przeciwnym przypadku. /* * TakNie * * Funkcja wyświetlająca okno Tak/Nie
Część I Programowanie w GTK+
128
*/ void TakNie (char *szKomunikat, void (*FunkcjaTak)(), void (*FunkcjaNie)()) { GtkWidget *etykieta; GtkWidget *przycisk; GtkWidget *okno_dialogowe; /* --- tworzymy okno dialogowe --- */ okno_dialogowe = gtk_dialog_new (); /* --- Przechwytujemy sygnał "destroy" aby zwolnić blokadę --- */ gtk_signal_connect (GTK_OBJECT (okno_dialogowe), "destroy", GTK_SIGNAL_FUNC (ZamkniecieOkna), &okno_dialogowe); /* --- ustawiamy tytuł --- */ gtk_window_set_title (GTK_WINDOW (okno_dialogowe), "TakNie"); /* --- dodajemy niewielkie obramowanie --- */ gtk_container_border_width (GTK_CONTAINER (okno_dialogowe), 5); /* -------------------------------- */ /* --- tworzymy komunikat --- */ /* -------------------------------- */ /* --- tworzymy etykietę z komunikatem --- */ etykieta = gtk_label_new (szKomunikat); /* --- trochę miejsca na etykietę --- */ gtk_misc_set_padding (GTK_MISC (etykieta), 10, 10); /* --- dodajemy etykietę do okna dialogowego --- */ gtk_box_pack_start (GTK_BOX (GTK_DIALOG (okno_dialogowe)->vbox), etykieta, TRUE, TRUE, 0); /* --- wyświetlamy etykietę --- */ gtk_widget_show (etykieta); /* ---------------------- */ /* --- Przycisk Tak --- */ /* ---------------------- */ /* --- tworzymy przycisk "tak" --- */ przycisk = gtk_button_new_with_label ("Tak");
Dalsze kontrolki: ramki, tekst, okna dialogowe, okno wyboru pliku, pasek postępów gtk_signal_connect (GTK_OBJECT (przycisk), "clicked", GTK_SIGNAL_FUNC (FunkcjaTak), okno_dialogowe); GTK_WIDGET_SET_FLAGS (przycisk, GTK_CAN_DEFAULT); /* --- dodajemy przycisk do okna dialogowego --- */ gtk_box_pack_start (GTK_BOX ( GTK_DIALOG (okno_dialogowe)->action_area), przycisk, TRUE, TRUE, 0); /* --- uwidaczniamy przycisk --- */ gtk_widget_show (przycisk); /* ---------------------- */ /* --- Przycisk Nie --- */ /* ---------------------- */ /* --- tworzymy przycisk "nie" --- */ przycisk = gtk_button_new_with_label ("Nie"); gtk_signal_connect (GTK_OBJECT (przycisk), "clicked", GTK_SIGNAL_FUNC (FunkcjaNie), okno_dialogowe); /* --- Pozwalamy, aby przycisk "nie" był domyślnym --- */ GTK_WIDGET_SET_FLAGS (przycisk, GTK_CAN_DEFAULT); /* --- dodajemy przycisk "nie" do okna dialogowego --- */ gtk_box_pack_start (GTK_BOX ( GTK_DIALOG (okno_dialogowe)->action_area), przycisk, TRUE, TRUE, 0); /* --- Czynimy przycisk "nie" domyślnym --- */ gtk_widget_grab_default (przycisk); /* --- Uwidaczniamy przycisk --- */ gtk_widget_show (przycisk); /* --- Wyświetlamy okno dialogowe --- */ gtk_widget_show (okno_dialogowe); /* --- Teraz można używać tylko tego okna --- */ gtk_grab_add (okno_dialogowe); }
129
Część I Programowanie w GTK+
130
Funkcję TakNie można teraz wykorzystać w aplikacjach do tworzenia zapierających dech w piersiach okien dialogowych. Wywołanie funkcji wygląda następująco: TakNie ("Czy na pewno chcesz pobrać " "wszystkie pieniądze z konta Billa Gatesa?", WyswietlTak, WyswietlNie);
Funkcje WyswietlTak i WyswietlNie powinny usuwać okno dialogowe, kiedy zakończą przetwarzanie. Funkcja TakNie przekazuje wskaźnik GtkPointer do okna dialogowego jako parametr gpointer funkcji zwrotnej dla sygnału "clicked". Funkcje WyswietlTak i WyswietlNie są tutaj niewielkie, ale nic nie stoi na przeszkodzie, aby były większe i wykonywały dowolne operacje: /* * WyswietlTak * * Pokazuje, że wciśnięto przycisk "Tak" */ void WyswietlTak (GtkWidget *kontrolka, gpointer dane) { /* --- Wyświetlamy komunikat --- */ g_print ("Wyciągam $60 000 000 000,00 z konta\n"); /* --- zamykamy okno --- */ gtk_widget_destroy (GTK_WIDGET (dane)); } /* * WyswietlNie * * Pokazuje, że wciśnięto przycisk "Nie" */ void WyswietlNie (GtkWidget *kontrolka, gpointer dane) { /* --- Wyświetlamy komunikat --- */ g_print ("Pieniądze i tak nie dają szczęścia\n"); /* --- zamykamy okno --- */ gtk_widget_destroy (GTK_WIDGET (dane)); }
Dalsze kontrolki: ramki, tekst, okna dialogowe, okno wyboru pliku, pasek postępów
131
Okno wyboru pliku Okno wyboru pliku jest wyspecjalizowanym oknem dialogowym, udostępniającym standardowy sposób wyboru pliku i podobnym do uniwersalnego okna operacji na pliku znanego z Windows (Common Dialog). Dzięki niemu aplikacje GTK+ mogą posiadać wspólny wygląd i styl. Okno to pozwala na wybranie pliku przez użytkownika i przeprowadzenie pewnej czynności na wybranym pliku (patrz rysunek 6.4).
Rysunek 6.4. Okno wyboru pliku.
Okno wyboru pliku w GTK+ tworzy się przy pomocy funkcji gtk_file_selection_new, do której należy przekazać tytuł okna. Dobrze jest ustawić tytuł na „Zapisz jako” albo „Otwórz”, na wypadek, gdyby użytkownik zapomniał, co właściwie chce zrobić, albo musiał wstać od komputera akurat wtedy, kiedy na ekranie pojawia się okno. /* --- tworzymy okno dialogowe z tytułem --- */ plikw = gtk_file_selection_new ("Zapisz jako");
Jeśli okno dialogowe zostało wyświetlone dlatego, że użytkownik wybrał z menu opcję „Zapisz jako”, wówczas pole nazwy pliku w oknie dialogowym powinno zawierać bieżącą nazwę pliku (mogą też istnieć inne powody ustawienia domyślnej wartości tego pola). Można ustawić domyślną nazwę pliku przy pomocy funkcji gtk_file_selection_set_filename. /* --- ustawiamy domyślną nazwę pliku --- */ gtk_file_selection_set_filename (GTK_FILE_SELECTION (plikw), "problemy.txt");
Część I Programowanie w GTK+
132
Okno wyboru pliku posiada zbiór kontrolek, do których można uzyskać dostęp przez strukturę GtkFileSelection, zdefiniowaną w pliku gtkfilesel.h w następujący sposób: struct _GtkFileSelection { GtkWindow window; GtkWidget GtkWidget GtkWidget GtkWidget GtkWidget GtkWidget GtkWidget GtkWidget GtkWidget GtkWidget GList GtkWidget GtkWidget gchar gpointer
*dir_list; *file_list; *selection_entry; *selection_text; *main_vbox; *ok_button; *cancel_button; *help_button; *history_pulldown; *history_menu; *history_list; *fileop_dialog; *fileop_entry; *fileop_file; cmpl_state;
GtkWidget *fileop_c_dir; GtkWidget *fileop_del_file; GtkWidget *fileop_ren_file; GtkWidget *button_area; GtkWidget *action_area; };
Najczęściej używanymi kontrolkami są przyciski ok_button, cancel_button i help_button. Dla większości pozostałych kontrolek zdefiniowane są domyślne reakcje, których zazwyczaj nie trzeba zmieniać. Przyciski są częścią okna wyboru pliku i można zarejestrować dla nich swoje własne zdarzenia. Jeśli chcielibyśmy na przykład zostać poinformowani, że użytkownik nacisnął klawisz „Cancel”, musielibyśmy umieścić w programie fragment kodu, który ustawiałby procedurę obsługi zdarzenia dla kontrolki cancel_button w strukturze GtkFileSelection. gtk_signal_connect_object ( GTK_OBJECT (GTK_FILE_SELECTION (plikw)->cancel_button),
Dalsze kontrolki: ramki, tekst, okna dialogowe, okno wyboru pliku, pasek postępów
133
"clicked", (GtkSignalFunc) gtk_widget_destroy, GTK_OBJECT (plikw));
Funkcja gtk_signal_connect_object działa podobnie do gtk_signal_connect ustawia funkcje zwrotne dla sygnałów, ale wywoływana funkcja zwrotna otrzymuje tylko czwarty parametr funkcji gtk_signal_connect_object. Powoduje to, że funkcja zwrotna nie jest w stanie stwierdzić, która kontrolka ją wywołała. Funkcję zwrotną należy zdefiniować następująco: gint FunkcjaZwrotna (GtkObject *kontrolka) { }
Obiekt, przesłany do funkcji zwrotnej, byłby w naszym przykładzie oknem wyboru pliku, co skonfigurowaliśmy w wywołaniu funkcji gtk_signal_connect_object. Odniesienie do obiektu, który spowodował wywołanie funkcji (w tym przypadku jest to przycisk „Cancel”) zostanie usunięte, ale w tym przypadku nie ma to znaczenia. Przycisk „Ok” można skonfigurować tak, aby przeprowadzał jakąś operację na pliku, przy pomocy podobnej procedury obsługi zdarzenia. Jeśli zaś aplikacja ma być przyjazna dla użytkownika, można zaopatrzyć w odpowiednią procedurę przycisk „Help” (można też usunąć go z okna, aby użytkownik nie nabrał fałszywego przekonania, że aplikacja zamierza mu w czymś pomóc, ale - naturalnie - nie jest to działanie przyjazne dla użytkownika).
Pasek postępów Pasek postępów (GtkProgressBar) jest niewielką, elegancką kontrolką, używaną często w celu zobrazowania bieżącego stanu funkcji, których wykonanie zabiera dużo czasu. Wyświetlenie postępów jest z pewnością lepsze, niż pozostawienie użytkownika w niepewności co do czasu zakończenia operacji. Twórca programu posiada pełną kontrolę nad paskiem postępów, ponieważ do uaktualniania stanu kontrolki trzeba napisać odpowiedni kod. GtkProgressBar dziedziczy wiele funkcji z kontrolki GtkProgress. Obie te kontrolki posiadają sporo możliwości, których nie będziemy tu opisywać. Stworzymy podstawową wersję paska postępów, której będziemy używać w dalszych przykładach podczas wykonywania długotrwałych operacji (patrz rysunek 6.5).
Część I Programowanie w GTK+
134
Kontrolkę paska postępów tworzymy przy pomocy funkcji gtk_progress_ bar_new albo gtk_progress_bar_new_with_adjustment. Po stworzeniu paska możemy uaktualnić go przy pomocy funkcji gtk_progress_ set_percent, do której przekazujemy procentową wartość ukończenia operacji, gdzie 0 oznacza ścisły początek, a 1 zakończenie operacji. Kontrolka będzie korzystać ze struktury, przechowującej informacje na temat paska postępów. Potrzebne dane to używane okno dialogowe, kontrolka paska postępów, znacznik, który wskazuje, czy dozwolone jest zamknięcie okna dialogowego, oraz informacja o procencie ukończenia operacji, wyświetlona ostatnio na pasku postępów. typedef struct { GtkWidget *pasekpostepow; GtkWidget *okno; int bPostep; int nOstatniProcent; } typDanePostepu;
Rysunek 6.5. Pasek postępów.
Będziemy sterować naszym paskiem postępów przy pomocy trzech funkcji. Pierwsza to ZacznijPostep. Utworzy ona pasek postępów w oknie dialogowym. /* * ZacznijPostep * * Tworzy okno dla paska postępów */ void ZacznijPostep () { GtkWidget *etykieta; GtkWidget *tabela; GtkWidget *okno; GtkAdjustment *reg; pdane = g_malloc (sizeof (typDanePostepu));
Dalsze kontrolki: ramki, tekst, okna dialogowe, okno wyboru pliku, pasek postępów
135
pdane->nOstatniProcent = -1; pdane->bPostep = TRUE; /* * --- tworzymy okno najwyższego poziomu */ okno = gtk_window_new (GTK_WINDOW_TOPLEVEL); pdane->okno = okno; /* --- Podłączamy sygnał "delete_event" --- */ gtk_signal_connect (GTK_OBJECT (okno), "delete_event", GTK_SIGNAL_FUNC (CzyMoznaZamknacOkno), pdane); gtk_container_border_width (GTK_CONTAINER (okno), 10); /* --- tworzymy tabelę --- */ tabela = gtk_table_new (3, 2, TRUE); gtk_container_add (GTK_CONTAINER (okno), tabela); /* --- dodajemy etykietę do tabeli --- */ etykieta = gtk_label_new ("Otwieram plik..."); gtk_table_attach_defaults (GTK_TABLE (tabela), etykieta, 0,2,0,1); gtk_widget_show (etykieta); /* --- dodajemy pasek postępów do tabeli --- */ reg = (GtkAdjustment *) gtk_adjustment_new (0, 0, 400, 0, 0, 0); pdane->pasekpostepu = gtk_progress_bar_new_with_adjustment (reg); gtk_table_attach_defaults (GTK_TABLE (tabela), pdane->pasekpostepu, 0,2,1,2); gtk_widget_show (pdane->pasekpostepu); /* --- uwidaczniamy wszystkie kontrolki --- */ gtk_widget_show (tabela); gtk_widget_show (okno); }
Funkcja UaktualnijPostep będzie obrazowała na pasku postępów bieżący stan długotrwałej operacji. Będzie przyjmować dwie wartości liczbowe bieżącą wartość stanu i ostateczną wartość stanu. Kiedy bieżąca wartość stanu będzie równa ostatecznej, będzie to oznaczać, że operacja została zakończona. Moglibyśmy użyć tej funkcji podczas wczytywania dużego pliku; ostateczną wartością stanu byłby wówczas rozmiar pliku, a bieżącą wartością - liczba wczytanych do tej pory bajtów.
Część I Programowanie w GTK+
136
/* * UaktualnijPostep * * Uaktualnia pasek postępów, aby odzwierciedlić * postępy we wczytywaniu pliku. * * poz - jaka część pliku została wczytana. * dlug - długość pliku * (poz / dlug) = % wczytania pliku */ void UaktualnijPostep (long poz, long dlug) { gfloat pwartosc; int procent; /* --- zapobiegamy dzieleniu przez zero --- */ if (dlug > 0) { /* --- obliczamy procent --- */ pwartosc = (gfloat) poz / (gfloat) dlug; procent = pwartosc * 100; if (pdane->nOstatniProcent != procent) { /* --- Uaktualniamy wyświetlaną wartość --- */ gtk_progress_set_percentage (
➥GTK_PROGRESS (pdane->pasekpostepu), pwartosc); /* --- Odświeżamy okna - także okno postępów --- */ while (gtk_events_pending ()) { gtk_main_iteration (); } pdane->nOstatniProcent = procent; } } }
Funkcja ZakonczPostep zamyka okno dialogowe z paskiem postępów. Powinna zostać wywołana po zakończeniu operacji w celu zamknięcia okna. /*
Dalsze kontrolki: ramki, tekst, okna dialogowe, okno wyboru pliku, pasek postępów
137
* ZakonczPostep * * Zamyka okno z paskiem postępów */ void ZakonczPostep () { /* --- Pozwalamy na zamknięcie okna --- */ pdane->bPostep = FALSE; /* --- usuwamy okno --- */ gtk_widget_destroy (pdane->okno); /* --- zwalniamy przydzieloną pamięć --- */ g_free (pdane); pdane = NULL; }
Funkcja CzyMoznaZamknacOkno uniemożliwia użytkownikowi zamknięcie okna, zanim operacja nie dobiegnie końca. Jest wywoływana przez sygnał "delete_event", wysyłany, kiedy użytkownik spróbuje zamknąć okno. Wartość powrotna określa, czy można zamknąć okno. /* * CzyMoznaZamknacOkno * * Funkcja ta sprawdza, czy można zamknąć okno dialogowe. */ gint CzyMoznaZamknacOkno (GtkWidget *kontrolka) { /* --- TRUE => nie można zamknąć --- */ /* --- FALSE => można zamknąć --- */ return (pdane->bPostep); }
Poniżej zamieszczamy przykład, ilustrujący działanie paska postępów. Tworzy on okno, zawierające przycisk. Po naciśnięciu przycisku tworzone jest (przy pomocy funkcji ZacznijPostep) okno dialogowe z paskiem postępów, który jest uaktualniany co 100 milisekund przy pomocy zegara.
Część I Programowanie w GTK+
138 Korzystanie z zegara
Zegar tworzy się przy pomocy funkcji gtk_timeout_add. Funkcja ta przyjmuje częstotliwość zegara, inną funkcję i parametr danych. Funkcja, przekazana jako parametr do gtk_timeout_add będzie wywoływana z zadaną częstotliwością, przy czym będzie do niej przekazywany parametr danych. Na przykład następujące wywołanie funkcji: pzegar = gtk_timeout_add (100, UaktualnijZegarPaska, dane);
spowoduje wywoływanie co 100 milisekund funkcji UaktualnijZegarPaska z parametrem dane. Wartością zwrotną funkcji gtk_timeout_add jest identyfikator zegara, który jest potrzebny do zakończenia jego pracy. Aby zaprzestać wywoływania funkcji UaktualnijZegarPaska, należy skorzystać z funkcji gtk_timeout_remove. Przekazujemy do niej identyfikator, otrzymany z wywołania gtk_timeout_add. gtk_timeout_remove (pzegar);
Test paska postępów Oto program, służący do przetestowania działania paska postępów: /* * Tutaj zaczyna się kod aplikacji */ #include int pzegar; int nWartosc; /* * UaktualnijZegarPaska * * Jest to funkcja zwrotna zegara. Uaktualnia pasek postępów * i zamyka jego okno, kiedy pasek osiągnie końcową wartość. */ UaktualnijZegarPaska (gpointer dane) { /* --- wartość przyrostu --- */ nWartosc += 1; /* --- uaktualniamy pasek postępów --- */ UaktualnijPostep (nWartosc, 100);
Dalsze kontrolki: ramki, tekst, okna dialogowe, okno wyboru pliku, pasek postępów /* --- Zamykamy go, jeśli osiągnął końcową wartość --- */ if (nWartosc == 100) { ZakonczPostep (); gtk_timeout_remove (pzegar); } } /* * KliknietoPrzycisk * * Wywoływana po kliknięciu przycisku w celu utworzenia zegara * i paska postępów. */ gint KliknietoPrzycisk (GtkWidget *kontrolka, gpointer dane) { /* --- Inicjacja --- */ nWartosc = 0; ZacznijPostep (); /* --- Wywołujemy zegar --- */ pzegar = gtk_timeout_add (100, UaktualnijZegarPaska, dane); } /* * ZamknijOknoAplikacji * * Zamyka aplikację */ gint ZamknijOknoAplikacji () { gtk_main_quit (); return (FALSE); } /* * main * * tutaj zaczyna się program */ main (int argc, char *argv[]) { GtkWidget *okno;
139
Część I Programowanie w GTK+
140 GtkWidget *przycisk; gtk_init (&argc, &argv);
/* --- tworzymy główne okno i nadajemy mu tytuł --- */ okno = gtk_window_new (GTK_WINDOW_TOPLEVEL); gtk_window_set_title (GTK_WINDOW (okno), "Pasek postêpów"); /* --- kończymy aplikację po wykryciu sygnału delete_event --- */ gtk_signal_connect (GTK_OBJECT (okno), "delete_event", GTK_SIGNAL_FUNC (ZamknijOknoAplikacji), NULL); /* --- tworzymy przycisk, który wyświetli pasek postępów ...--- */ przycisk = gtk_button_new_with_label ("Pasek postêpów"); gtk_widget_show (przycisk); /* --- ...a tutaj ustawiamy jego procedurę obsługi --- */ gtk_signal_connect (GTK_OBJECT (przycisk), "clicked", GTK_SIGNAL_FUNC (KliknietoPrzycisk), NULL); gtk_container_add (GTK_CONTAINER (okno), przycisk); gtk_widget_show (okno); /* --- Oddajemy sterowanie do GTK --- */ gtk_main (); exit (0); }
Podsumowanie Kontrolki opisane w tym rozdziale są bardziej skomplikowane, niż proste kontrolki, którymi zajmowaliśmy się wcześniej. Okna dialogowe są używane w GTK+ bardzo często, a przykładem ich zastosowania jest okno wyboru pliku. Okno wyboru pliku jest standardowym oknem dialogowym, pozwalającym użytkownikowi na wybieranie plików. Kontrolka tekstu jest niemal gotowym edytorem. Pasek postępów może zobrazować postępy w wykonaniu długotrwałej operacji. Wszystkie te kontrolki znacznie ułatwiają pisanie aplikacji dla Linuksa.
Rozdział 7 Aplikacja kalkulatora W rozdziale tym wykorzystamy zdobytą wiedzę, aby stworzyć prosty kalkulator. Aplikacja tego typu stanowi dobry przykład, ponieważ składa się z grupy przycisków i pola wyświetlającego rezultaty. Przyciski ułożone są w tabeli pakującej, pod polem rezultatów. Aplikacja pozwala posługiwać się kalkulatorem zarówno przy pomocy myszy, jak i klawiatury i przeprowadza podstawowe działania arytmetyczne.
Program kalkulatora Istotnym aspektem pracy kalkulatora jest wizualne potwierdzanie operacji, przeprowadzanych przez użytkowników. Jeśli użytkownik kliknie przycisk kalkulatora, powinien mieć pewność, że przycisk został wciśnięty - to znaczy, przycisk powinien opuścić się i podnieść z powrotem. Oczywiście, pole rezultatów powinno zostać uaktualnione odpowiednio do klikniętego przycisku. Jednakże niektórzy użytkownicy wolą posługiwać się klawiaturą, niż myszą - oni również powinni otrzymać potwierdzenie swoich czynności. Nasza aplikacja będzie przesuwać ognisko na ekranowy przycisk, odpowiadający klawiszowi na klawiaturze. Dzięki temu użytkownicy wprowadzający dane w ten sposób również zobaczą podświetlenie przycisku i uaktualnione pole rezultatów.
Struktury danych Każdy przycisk w kalkulatorze posiada zbiór związanych z nim właściwości: wskaźnik do kontrolki, etykietę oraz pozycję w rzędzie i kolumnie, która określa, gdzie należy go umieścić wewnątrz tabeli pakującej. Struktura danych dla każdego przycisku wygląda następująco: /* * --- struktura przechowująca dane o przyciskach
Część II Przykładowe aplikacje w GTK+
146
*/ typedef struct { char *szEtykieta; /*---etykieta wyświetlana na przycisku---*/ int wiersz; /*---rząd, w którym należy umieścić przycisk---*/ int kol; /*---kolumna, w którym należy umieścić przycisk---*/ GtkWidget *kontrolka; /*---wskaźnik do przycisku---*/ } typPrzyciskKalkulatora;
Użyjemy tej struktury do zdefiniowania tablicy przycisków. Jest to tablica elementów typu typPrzyciskKalkulatora. Korzyścią, jaką odnosimy definiując przyciski w ten sposób, jest możliwość łatwego dodawania lub usuwania przycisków bez znaczących zmian w kodzie. Możemy także dowolnie przestawiać przyciski w oknie, nie zmieniając reszty programu. Przyciski kalkulatora są zdefiniowane następująco: typPrzyciskKalkulatora listaPrzyciskow [] = { {"C", 1, 0, NULL}, /* --- Czyszczenie --- */ {"CE", 1, 1, NULL}, /* --- Czyszczenie --- */ {"/", 1, 3, NULL}, /* --- Dzielenie --- */ {"7", {"8", {"9", {"*", {"%",
2, 0, 2, 1, 2, 2, 2, 3, 2, 4,
NULL}, NULL}, NULL}, NULL}, NULL},
/* --- Cyfra --- */ /* --- Cyfra --- */ /* --- Cyfra --- */ /* --- Dzielenie --- */ /* --- Procent --- */
{"4", {"5", {"6", {"-", {"1/x",
3, 0, 3, 1, 3, 2, 3, 3, 3, 4,
NULL}, NULL}, NULL}, NULL}, NULL},
/* --- Cyfra --- */ /* --- Cyfra --- */ /* --- Cyfra --- */ /* --- Odejmowanie --- */ /* --- 1/x --- */
{"1", {"2", {"3", {"+", {"pierw",
4, 0, 4, 1, 4, 2, 4, 3, 4, 4,
NULL}, NULL}, NULL}, NULL}, NULL},
/* --- Cyfra --- */ /* --- Cyfra --- */ /* --- Cyfra --- */ /* --- Dodawanie --- */ /* --- Pierwiastek --- */
{"+/-", {"0", {".", {"=", {"x^2",
5, 0, 5, 1, 5, 2, 5, 3, 5, 4,
NULL}, NULL}, NULL}, NULL}, NULL},
/* --- Zanegowanie --- */ /* --- zero --- */ /* --- Kropka dziesiętna --- */ /* --- Równa się/suma --- */ /* --- Kwadrat --- */
Aplikacja kalkulatora
147
};
Poniższy wzór oblicza liczbę przycisków w tablicy: int nPrzyciski = sizeof (listaPrzyciskow) / sizeof (typPrzyciskKalkulatora)
Wzór ten automatycznie oblicza liczbę przycisków w tablicy w czasie kompilacji, abyśmy nie musieli ustawiać jej ręcznie po dodaniu albo usunięciu przycisku. Jest to z pewnością wygodniejsza metoda, niż użycie stałej opisującej rozmiar tablicy, którą należałoby uaktualniać za każdym razem, kiedy dodalibyśmy albo usunęli przycisk kalkulatora. Oprócz tablicy z informacjami o przyciskach (listaPrzyciskow) aplikacja potrzebuje także innych zmiennych globalnych, na przykład wskaźnika do etykiety, używanej do wyświetlania rezultatów. Wyświetlacz LCD kalkulatora jest reprezentowany przez etykietę, ponieważ kontrolowanie wpisywanych danych jest o wiele łatwiejsze, jeśli użytkownik nie może wpisywać ich bezpośrednio do kontrolki. Wszystkie naciśnięcia klawiszy są przechwytywane przez okno aplikacji. Okno aplikacji uaktualnia etykietę po odfiltrowaniu znaków, które nie powinny się w niej znaleźć.
Główny program Po wyjaśnieniu założeń dotyczących kalkulatora oraz opisie struktur danych możemy zacząć pisanie kodu. Funkcja main przeprowadza czynności wstępne, konieczne do uruchomienia kalkulatora. Należy utworzyć i rozmieścić okna oraz przypisać odpowiednie funkcje zwrotne, a następnie wywołać funkcję gtk_main, aby rozpocząć przetwarzanie sygnałów. Zaczynamy od inicjacji GTK+ i utworzenia okna aplikacji. int main (int argc, char *argv[]) { GtkWidget *okno; GtkWidget *tabela; /* --- inicjacja GTK --- */ gtk_init (&argc, &argv); /* --- tworzymy okno kalkulatora --- */ okno = gtk_window_new (GTK_WINDOW_TOPLEVEL);
Powinniśmy nadać aplikacji opisowy tytuł i ustawić minimalny rozmiar okna, ponieważ domyślne okno jest nieco za małe, aby użytkownik mógł wygodnie klikać przyciski.
148
Część II Przykładowe aplikacje w GTK+
/* --- Nadajemy oknu tytuł --- */ gtk_window_set_title (GTK_WINDOW (okno), "Kalkulator"); /* --- Ustawiamy minimalny rozmiar okna --- */ gtk_widget_set_usize (okno, 200, 200);
Jak zwykle, nie należy zapominać o przechwyceniu sygnału, informującego o zamykaniu okna aplikacji. Okno aplikacji oczekuje także na sygnał key_press_event, aby użytkownik mógł posługiwać się klawiaturą, a nie tylko myszą. Wciśnięcia klawiszy muszą zostać odwzorowane na odpowiednie przyciski, przy użyciu zdefiniowanej wcześniej tablicy, i obsłużone tak, jakby użytkownik kliknął dany przycisk myszą. /* --- musimy wiedzieć, że naciśnięto klawisz --- */ gtk_signal_connect (GTK_OBJECT (okno), "key_press_event", GTK_SIGNAL_FUNC (wcisnieto_klawisz), NULL); /* --- Zawsze należy podłączyć sygnał delete_event * do głównego okna. --- */ gtk_signal_connect (GTK_OBJECT (okno), "delete_event", GTK_SIGNAL_FUNC (ZamknijOknoApl), NULL);
Tabela pakująca, w której umieścimy przyciski, ma rozmiary 5 x 5, przy czym pierwszy rząd służy do przechowywania wyświetlacza. Funkcja UtworzPrzyciskiKalkulatora odczytuje tablicę przycisków i rozmieszcza je we właściwych punktach tabeli pakującej. Dodaje także wspólną procedurę obsługi zdarzenia dla wszystkich przycisków, która będzie wywoływana w razie kliknięcia przycisku albo naciśnięcia klawisza. /* --- Tworzymy tabelę 5x5 dla elementów kalkulatora --- */ tabela = gtk_table_new (5, 5, TRUE); /* --- Tworzymy przyciski kalkulatora --- */ UtworzPrzyciskiKalkulatora (tabela);
Pole wyświetlacza jest kontrolką etykiety, wyświetlającą tekst wyrównany do prawej strony, a więc w sposób znany z prawdziwych kalkulatorów. Wyświetlacz jest dodawany do pierwszego rzędu tabeli i rozciąga się na pięć kolumn. /* --- Tworzymy wyświetlacz LCD kalkulatora --- */ etykieta = gtk_label_new ("0"); gtk_misc_set_alignment (GTK_MISC (etykieta), 1, .5); /* --- Dodajemy etykietę do tabeli --- */
Aplikacja kalkulatora
149
gtk_table_attach_defaults (GTK_TABLE (tabela), etykieta, 0, 4, 0, 1);
Następnie uwidaczniamy wszystkie kontrolki i dodajemy je do głównego okna. gtk_widget_show (etykieta); /* --- Uwidaczniamy tabelę i okno --- */ gtk_container_add (GTK_CONTAINER (okno), tabela); gtk_widget_show (tabela); gtk_widget_show (okno);
Wreszcie wywołujemy gtk_main, aby rozpocząć przetwarzanie zdarzeń. gtk_main (); exit (0); }
Procedura obsługi delete_event zamknie GTK+, kiedy użytkownik zakończy aplikację kalkulatora. /* * ZamknijOknoApl * * Okno jest zamykane, wychodzimy z pętli GTK */ gint ZamknijOknoApl (GtkWidget *widget, gpointer data) { gtk_main_quit (); return (FALSE); }
Utwórz Przyciski Kalkulatora Funkcja UtworzPrzyciskiKalkulatora tworzy przyciski na podstawie tablicy, opisanej we wcześniejszej części rozdziału. Każdemu tworzonemu przyciskowi przypisuje etykietę, rząd i kolumnę określoną w tablicy. Wskaźnik GtkWidget, reprezentujący nowo utworzony przycisk, jest zapisywany w tablicy. Funkcja UtworzPrzyciskiKalkulatora korzysta z funkcji UtworzPrzycisk, która tworzy jeden przycisk. void UtworzPrzyciskiKalkulatora (GtkWidget *tabela) {
Część II Przykładowe aplikacje w GTK+
150 int nIndeks;
/* --- Przechodzimy przez listę przycisków --- */ for (nIndeks = 0; nIndeks < nPrzyciski; nIndeks++) { /* --- Tworzymy przycisk --- */ listaPrzyciskow[nIndeks].kontrolka = UtworzPrzycisk (tabela, listaPrzyciskow[nIndeks].szEtykieta, listaPrzyciskow[nIndeks].wiersz, listaPrzyciskow[nIndeks].kol); } }
Utwórz Przycisk Funkcja UtworzPrzycisk tworzy przycisk z etykietą i umieszcza go w tablicy pakującej. Funkcja ta ustawia także procedurę obsługi zdarzenia "clicked", kliknieto_przycisk. Zauważmy, że wszystkie przyciski korzystają z tej samej funkcji zwrotnej kliknieto_przycisk. Zamiast pobierać etykietę z przycisku (w celu sprawdzenia, który przycisk spowodował zdarzenie), łatwiej jest skonfigurować funkcję zwrotną tak, aby otrzymywała tę etykietę jako parametr. Po odpowiednim ustawieniu funkcji kliknieto_ przycisk w wywołaniu gtk_signal_connect, będzie ona otrzymywała etykietę przycisku w parametrze typu gpointer. /* * UtworzPrzycisk * * Tworzy przycisk, przypisuje procedury obsługi zdarzenia, i * umieszcza przycisk we właściwej komórce tabeli pakującej */ GtkWidget *UtworzPrzycisk (GtkWidget *tabela, char *szEtykieta,
➥ int wiersz, int kolumna) { GtkWidget *przycisk; /* --- tworzymy przycisk --- */ przycisk = gtk_button_new_with_label (szEtykieta); /* --- musimy wiedzieć, kiedy przycisk zostanie kliknięty --- */ gtk_signal_connect (GTK_OBJECT (przycisk), "clicked", GTK_SIGNAL_FUNC (kliknieto_przycisk), szEtykieta);
Aplikacja kalkulatora
151
/* --- umieszczamy przycisk we właściwej komórce tabeli --- */ gtk_table_attach (GTK_TABLE (tabela), przycisk, kolumna, kolumna+1, wiersz, wiersz + 1, GTK_FILL | GTK_EXPAND, GTK_FILL | GTK_EXPAND, 5, 5); /* --- Uwidaczniamy przycisk --- */ gtk_widget_show (przycisk); /* --- Zwracamy przycisk --- */ return (przycisk); }
Procedura obsługi, która będzie przetwarzać zdarzenia pochodzące od przycisków i klawiszy, musi określić, który przycisk wciśnięto i na tej podstawie odpowiednio uaktualnić wyświetlacz kalkulatora. W przypadku kliknięć myszy zdarzenie otrzymuje przycisk, na którym kliknięto. Natomiast w przypadku naciśnięcia klawisza (key_press_event) zdarzenie prawdopodobnie otrzyma inny przycisk, niż chciał nacisnąć użytkownik. Jeśli na przykład ognisko (focus) spoczywa na przycisku „3”, a użytkownik naciśnie klawisz „5”, wówczas to przycisk „3” otrzyma sygnał o naciśnięciu klawisza „5”. Obsługa klawiszy powinna być podobna do obsługi sygnału clicked, gdzie sygnał otrzymuje kliknięty przycisk (po naciśnięciu klawisza „5” odpowiedni sygnał powinien otrzymać przycisk „5”). Co więcej, jeśli użytkownik korzysta z klawiatury, ognisko powinno przesuwać się na przycisk odpowiadający wciśniętemu klawiszowi; w omawianym przykładzie ognisko powinno przesunąć się na przycisk „5”, ponieważ wciśnięto klawisz „5”. Jest to dodatkowe, wizualne potwierdzenie czynności użytkownika, kwestia raczej estetyczna, niż programistyczna. Zamiast przypisywać poszczególnym przyciskom funkcję zwrotną dla sygnału key_press_event, możemy przypisać ją całemu oknu aplikacji, do którego trafiają wszystkie sygnały, które nie zostały obsłużone przez kontrolkę z ogniskiem. Funkcja zwrotna dla okna aplikacji, obsługująca sygnał key_press_event, wyśle sygnał clicked do właściwego przycisku. Zdarzenia kliknięcia myszy i naciśnięcia klawisza prowadzą do podjęcia przez program tych samych czynności. Procedura obsługi zdarzenia key_press_event może wykorzystać ten fakt, wywołując te same funkcje, które używane są podczas przetwarzania sygnału clicked.
152
Część II Przykładowe aplikacje w GTK+
Funkcja zwrotna dla key_press_event jest niewielka, ponieważ musi ona tylko określić, który przycisk należy kliknąć, na podstawie naciśniętego klawisza. Przegląda ona przyciski w tablicy i próbuje dopasować klawisz do tekstu, wyświetlanego na etykiecie przycisku. Porównywany jest tylko pierwszy znak etykiety, więc nie da się użyć tej procedury dla bardziej skomplikowanych operacji kalkulatora (na przykład 1/x), które trudno jest wyrazić pojedynczym wciśnięciem klawisza. Operuje ona tylko na częściej używanych symbolach, jak cyfry i proste operatory (+-/*). Po odnalezieniu właściwego przycisku funkcja zwrotna przesuwa na niego ognisko w celu wizualnego potwierdzenia operacji, a następnie wywołuje funkcję gtk_button_clicked, aby wygenerować sygnał clicked dla tego przycisku. /* * wcisnieto_klawisz * * Obsługuje sygnał "key_press_event" * * Funkcja poszukuje odpowiednika klawisza w strukturze * danych kalkulatora i (jeśli go znajdzie) klika w imieniu * użytkownika właściwy przycisk. Zmniejsza to nasz program, * ponieważ musimy obsłużyć tylko zdarzenia "clicked". */ void wcisnieto_klawisz (GtkWidget *kontrolka, GdkEventKey *zdarzenie, gpointer dane) { int nIndeks; /* --- przeglądamy przyciski --- */ for (nIndeks = 0; nIndeks < nPrzyciski; nIndeks++) { /* --- Jeśli wciśnięty klawisz odpowiada pierwszemu --- */ /* --- znakowi etykiety przycisku ORAZ długość etykiety --- */ /* --- jest równa jeden --- */ if (zdarzenie->keyval == listaPrzyciskow[nIndeks].szEtykieta[0] && listaPrzyciskow[nIndeks].szEtykieta[1] == (char) 0) { /* --- Przesuwamy ognisko na ten przycisk --- */ gtk_widget_grab_focus (listaPrzyciskow[nIndeks].kontrolka); /* --- Symulujemy kliknięcie przycisku --- */
Aplikacja kalkulatora
153
gtk_button_pressed (GTK_BUTTON ( listaPrzyciskow[nIndeks].kontrolka)); return; } } }
Jeśli użytkownik kliknie przycisk, albo naciśnie klawisz, zostanie wywołana funkcja obsługi zdarzenia kliknieto_przycisk. Funkcja ta przechowuje aktualny stan kalkulatora i przeprowadza żądane operacje, albo też dodaje cyfry do wyświetlacza przy pomocy funkcji gtk_label_set. Naturalnie, funkcja kliknieto_przycisk mogłaby obsługiwać pierwszeństwo operatorów, nawiasy, dalsze operacje (sin, cos, tan), a także konwersje na format szesnastkowy i binarny, ale pozostawimy to inwencji Czytelnika. Funkcja kliknieto_przycisk rozróżnia dwa podstawowe przypadki. Użytkownik albo wprowadza cyfrę (od 0 do 9) albo też żąda przeprowadzenia operacji. Jeśli użytkownik wciska cyfrę, zazwyczaj oznacza to, że należy po prostu dodać ją do wyświetlacza. Jeśli na wyświetlaczu znajduje się na przykład liczba „45”, a użytkownik naciśnie „2”, wówczas wyświetlacz powinien pokazać „452”. Istnieje wyjątek od tej reguły, jeśli poprzednio wprowadzonym znakiem był operator binarny (+-/*). Kiedy użytkownik kliknie operator binarny, wówczas wyświetlacz będzie nadal pokazywał liczbę, ale nie należy dodawać do niej nowej cyfry. Nowa cyfra jest częścią drugiego operandu i powinna zastąpić poprzednią liczbę. Z drugim przypadkiem mamy do czynienia wtedy, kiedy użytkownik wprowadza operator. W takim liczba na wyświetlaczu musi zostać użyta łącznie z liczbą, zapamiętaną wcześniej w buforze. Załóżmy, że użytkownik wpisał „235+111”. Do tej pory kalkulator nie przeprowadził żadnych obliczeń. Kiedy użytkownik wprowadzi znak „=”, wówczas kalkulator sprawdza poprzedni operator („+”), zapamiętaną w buforze liczbę („235”) oraz liczbę na wyświetlaczu. Teraz musi wykonać dodawanie „235” i „111”. Taka sama sytuacja miałaby miejsce, gdyby użytkownik nacisnął „+”, zamiast „=”, po wprowadzeniu „235+111”. Różnica polega na tym, że drugi znak „+” wprowadziłby kalkulator w tryb dodawania następnej liczby. /* * kliknieto_przycisk * * kontrolka - kliknięty przycisk * dane - etykieta przycisku *
Część II Przykładowe aplikacje w GTK+
154
* Kliknięto przycisk, obsługujemy go */ void kliknieto_przycisk (GtkWidget *kontrolka, gpointer dane) { char zn = *((char *) dane); char *lancuch; /* --- Pobieramy etykietę przycisku --- */ lancuch = (char *) dane; /* --- Wprowadzanie cyfry... --- */ if (ZnakZmiennoprzecinkowy (zn) && strlen (lancuch) == 1) { ObsluzCyfre (lancuch, zn); } else { /* --- Czyszczenie --- */ if (strcmp (lancuch, "CE") == 0) { gtk_label_set (GTK_LABEL (etykieta), "0"); return; /* --- DUŻE czyszczenie? --- */ } else if (strcmp (lancuch, "C") == 0) { poprzPolecenie = (char) 0; ostatniZnak = (char) 0; gtk_label_set (GTK_LABEL (etykieta), "0"); return; } else { /* --- Może to operator unarny? --- */ MozeOperatorUnarny (lancuch); } /* --- Sprawdzamy, czy jest do przeprowadzenia --- */ /* --- jakaś operacja binarna --- */ ObsluzOperacjeBinarna (); poprzPolecenie = zn; } ostatniZnak = zn; }
Aplikacja kalkulatora
155
Kod, który obsługuje cyfry, dodaje je na końcu wyświetlacza, o ile poprzednio wprowadzony klawisz nie był poleceniem. W takim przypadku cyfra staje się pierwszą w wyświetlaczu. /* * ObsluzCyfre * * Cyfra button was pressed, deal with it. How it * is dealt with depends on the situation. */ void ObsluzCyfre (char *lancuch, char zn) { char *tekstEtykiety; char bufor[BUF_SIZE]; int dlug; /* --- Jeśli poprzednio wprowadzono polecenie... --- */ if (Polecenie (ostatniZnak)) { /* --- czyścimy wyświetlacz --- */ gtk_label_set (GTK_LABEL (etykieta), ""); /* --- jeśli użytkownik wykonał obliczenie --- */ if (ostatniZnak == '=') { /* --- Zerujemy polecenie --- */ ostatniZnak = (char) 0; poprzPolecenie = (char) 0; } } /* --- Pobieramy tekst w etykiecie do bufora --- */ gtk_label_get (GTK_LABEL (etykieta), &tekstEtykiety); strcpy (bufor, tekstEtykiety); /* --- Dodajemy do niego nowy zmal --- */ dlug = strlen (bufor); bufor[dlug] = (gchar) zn; bufor[dlug+1] = (gchar) 0; /* --- Obcinamy wiodące zera. --- */ ObetnijWiodaceZera (bufor); /* --- Dodajemy znak do wyświetlacza --- */
Część II Przykładowe aplikacje w GTK+
156
gtk_label_set (GTK_LABEL (etykieta), (char *) bufor); }
Kalkulator obsługuje dwa typy poleceń: takie, które przyjmują dwie liczby w celu przeprowadzenia operacji, oraz takie, które przyjmują tylko jedną. Funkcja MozeOperatorUnarny sprawdza, czy polecenie jest operatorem unarnym, a jeśli tak, wówczas próbuje przeprowadzić unarną operację. /* * MozeOperatorUnarny * * lancuch - etykieta na przycisku, która opisuje operację * * Sprawdza, czy użytkownik nacisnął przycisk z operatorem * unarnym, jak %, pierw, 1/x etc., który należy obsłużyć * natychmiast */ void MozeOperatorUnarny (char *lancuch) { char *tekstEtykiety; char bufor[BUF_SIZE]; float num2; /* --- Pobieramy liczbę z wyświetlacza --- */ gtk_label_get (GTK_LABEL (etykieta), &tekstEtykiety); num2 = atof (tekstEtykiety); /* --- Procent? --- */ if (strcmp (lancuch, "%") == 0) { num2 = num2 / 100; /* --- Może 1/x? --- */ } else if (strcmp (lancuch, "1/x") == 0) { /* --- Nie można dzielić przez 0 --- */ if (num2 == 0) { return; } num2 = 1 / num2; /* --- Obliczanie pierwiastka --- */ } else if (strcmp (lancuch, "pierw") == 0) { num2 = sqrt ((double) num2);
Aplikacja kalkulatora
157
/* --- Obliczanie kwadratu --- */ } else if (strcmp (lancuch, "x^2") == 0) { num2 = num2 * num2; } /* --- Umieszczamy obliczoną liczbę w wyświetlaczu --- */ sprintf (bufor, "%f", (float) num2); ObetnijKoncoweZera (bufor); ObetnijWiodaceZera (bufor); gtk_label_set (GTK_LABEL (etykieta), bufor); }
Funkcja ObsluzOperacjeBinarna sprawdza, czy należy przeprowadzić operację binarną. Globalna zmienna num1 przechowuje pierwszą liczbę, która zostanie użyta w obliczeniach. Druga liczba jest pobierana z wyświetlacza kalkulatora. Kiedy obliczenia dobiegną końca, rezultat jest zapamiętywany w globalnej zmiennej num1, ponieważ może stać się operandem w kolejnej operacji binarnej. void ObsluzOperacjeBinarna () { char bufor[BUF_SIZE]; char *tekstEtykiety; float num2; /* --- Pobieramy liczbę z wyświetlacza --- */ gtk_label_get (GTK_LABEL (etykieta), &tekstEtykiety); num2 = atof (tekstEtykiety); /* --- Przeprowadzamy obliczenia na podstawie ostatnio --- */ /* --- wprowadzonego operatora --- */ switch (poprzPolecenie) { case '+': num1 = num1 + num2; break; case '-': num1 = num1 - num2; break; case '*': num1 = num1 * num2; break;
Część II Przykładowe aplikacje w GTK+
158 case '/': num1 = num1 / num2; break; case '=': num1 = num2; break; default: num1 = num2; break; }
/* --- Umieszczamy obliczoną liczbę w wyświetlaczu --- */ sprintf (bufor, "%f", (float) num1); ObetnijKoncoweZera (bufor); ObetnijWiodaceZera (bufor); gtk_label_set (GTK_LABEL (etykieta), bufor); }
Pozostałe funkcje są funkcjami pomocniczymi. Sprawiają, że program staje się czytelniejszy, ponieważ ich nazwy dokumentują przeprowadzane przez niego operacje. Na przykład funkcja ZnakZmiennoprzecinkowy sprawdza, czy wprowadzany znak może występować w liczbie zmiennoprzecinkowej (0-9 albo kropka „.”). Łatwiej czyta się linię kodu w rodzaju: if (ZnakZmiennoprzecinkowy (zn)) { niż linię: if (isdigit (zn) || zn == ".") { /* * ObetnijKoncoweZera * * Pozbywamy się końcowych zer. * Funkcja przyjmuje łańcuch i obcina końcowe zera */ void ObetnijKoncoweZera (char *szCyfry) { int nIndeks; int bKropka = FALSE; int nPoz = -1;
Aplikacja kalkulatora
/* --- Przeglądamy łańcuch w pętli --- */ for (nIndeks = 0; nIndeks < strlen (szCyfry); nIndeks++) { /* --- Czy to kropka dziesiętna? --- */ if (szCyfry[nIndeks] == '.') { bKropka = TRUE; } /* --- Jeśli jesteśmy po prawej stronie kropki... --- */ if (bKropka) { /* --- Zero? Hmm... czy już do końca? --- */ if (szCyfry[nIndeks] == '0') { /* --- Jeśli nie mamy zapamiętanej pozycji... --- */ if (nPoz < 0) { /* --- ...to zapamiętujemy ją teraz --- */ nPoz = nIndeks; } } else { /* --- Usuwamy zapamiętaną pozycję. To nie ta. --- */ nPoz = -1; } } } /* --- Przycinamy łańcuch --- */ if (nPoz > 0) { szCyfry[nPoz] = (char) 0; } } /* * ObetnijWiodaceZera * * Usuwa wiodące zera. * * Przekształca liczby w rodzaju "0000012" na "12" */ void ObetnijWiodaceZera (char *szCyfry) {
159
Część II Przykładowe aplikacje w GTK+
160 int nPoz; if (szCyfry == NULL) return;
/* --- dopóki mamy kombinację: zero na przedzie... --- */ for (nPoz = 0; (szCyfry[nPoz] && szCyfry[nPoz] == '0'); nPoz++) { /* --- ...a następny znak również jest cyfrą... --- */ if (isdigit (szCyfry[nPoz+1])) { /* --- ...zastępujemy zero spacją --- */ szCyfry[nPoz] = ' '; } } } /* * Polecenie * * Zwraca wartość TRUE, jeśli znak jest poleceniem * operującym na dwóch liczbach. */ int Polecenie (char zn) { switch (zn) { case '+': case '-': case '/': case '*': case '=': return (TRUE); } return (FALSE); } /* * ZnakZmiennoprzecinkowy * * Zwraca TRUE, jeśli znak jest jednym z [0123456789.] */ int ZnakZmiennoprzecinkowy (char zn) {
Aplikacja kalkulatora
161
return (isdigit (zn) || zn == '.'); }
Podsumowanie Napisanie prostego kalkulatora z wykorzystaniem kontrolek przycisków i etykiety jest dość łatwe. Dorzucamy do kontrolek kilka funkcji obsługi zdarzenia i otrzymujemy gotową aplikację. Nic nie stoi na przeszkodzie, aby przystąpić do pisania nieco większego programu.
Rozdział 8 Tworzenie prostego edytora tekstu W rozdziale tym stworzymy niewielki edytor tekstu, przypominający Notatnik z Windows, korzystając z wiedzy wyniesionej z lektury poprzednich rozdziałów. Oprzemy się na projekcie istniejącej aplikacji i spróbujemy naśladować jej funkcje, ponieważ może okazać się to łatwiejsze, niż projektowanie programu od podstaw. Notatnik Windows pozwala użytkownikom na edycję i zapisywanie prostych plików tekstowych, ale jego możliwości nie są imponujące. Napisanie zbliżonego programu będzie jednak dobrym ćwiczeniem w tworzeniu aplikacji przy użyciu kontrolek. Aplikacja notatnika (patrz rysunek 8.1) ilustruje wiele zagadnień związanych z używaniem różnych kontrolek, jak menu, paski narzędziowe, okna dialogowe i obszar służący do edycji danych. Jest luźno oparta na programie Notatnik, dostarczanym wraz z Microsoft Windows. Można przystosować ją do swoich potrzeb, albo używać tak, jak jest, do szybkiej edycji niewielkich plików. Menu najwyższego poziomu składa się z pozycji „Plik”, „Edycja”, „Szukaj” i „Pomoc”. Menu „Plik” zawiera pozycję „Nowy”, która tworzy nowe dokumenty, pozycję „Otwórz”, która otwiera istniejące elementy, pozycję „Zapisz”, która zapisuje bieżący dokument, pozycję „Zapisz jako...”, która zapisuje bieżący dokument pod nową nazwą, oraz pozycję „Zakończ”, która kończy pracę aplikacji. Menu „Edycja” zawiera polecenia operujące na schowku: „Wytnij”, „Kopiuj” i „Wklej”. Menu „Szukaj” zawiera tylko pozycję „Znajdź”, a menu „Pomoc” - pozycję „O programie...”, która wyświetla informacje o autorze i wersji programu.
164
Część II Przykładowe aplikacje w GTK+
Rysunek 8.1. Gnotatnik.
Oprócz menu aplikacja posiada także pasek narzędziowy, aby użytkownicy mieli szybki dostęp do większości poleceń. Oryginalny Notatnik nie posiada paska narzędziowego, ale dodanie go nie jest trudne, a zwiększy funkcjonalność programu.
main.c Program zaczyna się podobnie, jak większość aplikacji GTK+ - tworzy okno i nadaje mu tytuł, a następnie tworzy pionowe pole pakujące, w którym zostanie umieszczone menu, pasek narzędziowy, oraz kontrolka tekstowa służąca do przeglądania i edycji tekstu. Kod tworzący menu i pasek narzędziowy umieścimy w funkcji UtworzMenu, a samo pole edycyjne zostanie utworzone w funkcji UtworzTekst. W aplikacji pojawia się pewien problem, związany z tworzeniem paska narzędziowego. Utworzenie ikon paska wymaga przekształcenia tekstowych danych piksmapy na GdkPixmap, aby można było zamienić je w kontrolkę piksmapy. Funkcja konwertująca dane tekstowe wymaga podania okna X Window jako jednego z parametrów. Zamiast uwidaczniać okno (co spowodowałoby utworzenie okna X Window), a następnie ładować dane dla paska narzędziowego i wyświetlać pasek, można sprawić, aby okno było niewidoczne w trakcie konfigurowania, przy pomocy funkcji gtk_realize_widget. Funkcja ta tworzy okno X Window dla kontrolki, której można używać bez wyświetlania okna. Zazwyczaj „realizowanie” kontrolki odbywa się w trakcie działania funkcji gtk_widget_show, ale tutaj chcemy utworzyć okno X Windows dla aplikacji zanim wyświetlimy okno aplikacji. Można zrezygnować z użycia funkcji
Tworzenie prostego edytora tekstu
165
gtk_realize_widget i przesunąć wywołanie funkcji UtworzMenu za funkcję uwidaczniającą okno, ale nie byłoby to najlepsze rozwiązanie, ponieważ wyświetlone okno nie byłoby jeszcze gotowe (nie zawierałoby menu i paska narzędziowego). /* * --- main * * tutaj zaczyna się program */ int main(int argc, char *argv[]) { GtkWidget *okno; GtkWidget *ypole; /* --- Rozruch GTK --- */ gtk_init(&argc, &argv); /* --- tworzymy okno najwyższego poziomu --- */ okno = gtk_window_new(GTK_WINDOW_TOPLEVEL); /* --- tytuł i obramowanie --- */ gtk_window_set_title (GTK_WINDOW (okno), "Gnotatnik"); gtk_container_border_width (GTK_CONTAINER (okno), 0); /* --- sprawdzamy, czy główne okno nie jest zamykane --- */ gtk_signal_connect (GTK_OBJECT(okno), "delete_event", GTK_SIGNAL_FUNC (ZamknijOknoApl), NULL); /* --- ustawiamy rozmiar okna --- */ gtk_widget_set_usize (GTK_WIDGET(okno), 200, 200); /* --- tworzymy pionowe pole pakujące dla kontrolek --- */ ypole = gtk_vbox_new (FALSE, 1); /* --- szerokość obramowania = 1 --- */ gtk_container_border_width (GTK_CONTAINER(ypole), 1); /* --- Dodajemy pionowe pole pakujące do okna --- */ gtk_container_add (GTK_CONTAINER(okno), ypole); /* --- Uwidaczniamy pole pakujące --- */ gtk_widget_show (ypole);
Część II Przykładowe aplikacje w GTK+
166
/* --- Wiążemy okno z oknem X, aby móc utworzyć piksmapy --- */ gtk_widget_realize (okno); /* --- Tworzymy menu, pasek narzędziowy i kontrolkę tekstu --- */ UtworzMenu (okno, ypole); UtworzTekst (okno, ypole); /* --- uwidaczniamy okno najwyższego poziomu --- */ gtk_widget_show (okno); /* --- Pętla przetwarzania zdarzeń --- */ gtk_main(); return(0); }
menu.c Kod definiujący ikony paska narzędziowego i tworzący menu znajduje się w tym pliku. Duża część funkcji jest taka sama, jak w przykładowym interfejsie, który napisaliśmy wcześniej. Pierwszy blok kodu w menu.c definiuje bitmapy dla paska narzędziowego. Powinny one być łatwo rozpoznawalne i jasno wyrażać swoje funkcje. Zazwyczaj powinniśmy ograniczyć liczbę używanych kolorów do najwyżej czterech. Zbyt wiele kolorów zaciemnia przeznaczenie bitmapy i utrudnia rozpoznanie jej funkcji. Ponieważ wiele aplikacji używa typowych ikon do przeprowadzania podobnych operacji, powinniśmy trzymać się standardów - nic tak nie mąci użytkownikom w głowie, jak zbiór zupełnie nowych ikon. Tutaj wykorzystamy ponownie ikony z wcześniejszych przykładów. Funkcja UtworzMenu, wywoływana z main, konfiguruje menu i tablicę skrótów klawiszowych, a na koniec wywołuje funkcję UtworzPasekNarzedziowy, żeby jak łatwo się domyślić - utworzyć pasek narzędziowy. Funkcje zwrotne dla elementów menu posiadają w nazwie przedrostek menu, aby odróżniały się od zwykłych funkcji. Na przykład funkcja menu_Nowy będzie wywoływana wtedy, kiedy użytkownik wybierze opcję menu Plik/Nowy. /* * UtworzMenu * * Tworzy menu / pasek narzędziowy związane z głównym oknem */ void UtworzMenu (GtkWidget *okno, GtkWidget *ypole)
Tworzenie prostego edytora tekstu
167
{ GtkWidget *pasekmenu; GtkWidget *menu; GtkWidget *elmenu; glowne_okno = okno; /* --- tworzymy tablicę skrótów --- */ grupa_skrotow = gtk_accel_group_new (); gtk_accel_group_attach (grupa_skrotow, GTK_OBJECT (okno)); /* --- pasek menu --- */ pasekmenu = gtk_menu_bar_new (); gtk_box_pack_start (GTK_BOX (ypole), pasekmenu, FALSE, TRUE, 0); gtk_widget_show (pasekmenu); /* --------------------- menu Plik -------------------- */ menu = UtworzPodmenuPaska (pasekmenu, "Plik"); elmenu = UtworzElementMenu (menu, "Nowy", "^N", "Tworzy nowy plik", GTK_SIGNAL_FUNC (menu_Nowy), "nowy"); elmenu = UtworzElementMenu (menu, "Otwórz", "^O", "Otwiera istniejący plik", GTK_SIGNAL_FUNC (menu_Otworz), "otwórz"); elmenu = UtworzElementMenu (menu, "Importuj RTF", "", "Importuje plik RTF", GTK_SIGNAL_FUNC (menu_ImportujRTF), "importuj rtf"); elmenu = UtworzElementMenu (menu, "Zapisz", "^S", "Zapisuje bieżący plik", GTK_SIGNAL_FUNC (menu_Zapisz), "zapisz"); elmenu = UtworzElementMenu (menu, "Zapisz jako...", "", "Zapisuje bieżący plik pod nową nazwą", GTK_SIGNAL_FUNC (menu_ZapiszJako), "zapisz jako"); elmenu = UtworzElementMenu (menu, NULL, NULL, NULL, NULL, NULL);
Część II Przykładowe aplikacje w GTK+
168
elmenu = UtworzElementMenu (menu, "Zakończ", "", "Czy istnieje bardziej wymowna opcja?", GTK_SIGNAL_FUNC (menu_Zakoncz), "zakoncz"); /* ------------------------ menu Edycja --------------------- */ menu = UtworzPodmenuPaska (pasekmenu, "Edycja"); elmenu = UtworzElementMenu (menu, "Wytnij", "^X", "Usuwa znaki i umieszcza je w schowku", GTK_SIGNAL_FUNC (menu_Wytnij), "wytnij"); elmenu = UtworzElementMenu (menu, "Kopiuj", "^C", "Umieszcza kopię znaków w schowku", GTK_SIGNAL_FUNC (menu_Kopiuj), "kopiuj"); elmenu = UtworzElementMenu (menu, "Wklej", "^V", "Wkleja znaki", GTK_SIGNAL_FUNC (menu_Wklej), "wklej"); /* ----------------------- Menu Szukaj --------------------- */ menu = UtworzPodmenuPaska (pasekmenu, "Szukaj"); elmenu = UtworzElementMenu (menu, "Znajdź", "^F", "Znajduje znaki ", GTK_SIGNAL_FUNC (menu_Znajdz), "znajdz"); /* ----------------------- Menu Pomoc -------------------- */ menu = UtworzPodmenuPaska (pasekmenu, "Pomoc"); elmenu = UtworzElementMenu (menu, "O programie", "", "Informacje o programie", GTK_SIGNAL_FUNC (menu_OProgramie), "o programie"); /* --- tworzymy pasek narzędziowy --- */ UtworzPasekNarzedziowy (ypole); }
Tworzenie prostego edytora tekstu
169
W końcowej części funkcji UtworzMenu wywoływana jest funkcja UtworzPasekNarzedziowy, która tworzy pasek narzędziowy. Większość bitmap paska narzędziowego weźmiemy z wcześniejszego przykładu, ilustrującego tworzenie graficznego interfejsu użytkownika. Pasek narzędziowy będzie zawierał zwykłe przyciski, do których przypiszemy te same funkcje, co do elementów menu. Na przykład przycisk „Nowy” będzie po kliknięciu wywoływał funkcję menu_Nowy, czyli tę samą, którą wywołuje opcja menu Plik/Nowy. /* * UtworzPasekNarzedziowy * * Tworzy pasek narzędziowy, zawierający często używane polecenia. */ void UtworzPasekNarzedziowy (GtkWidget *ypole) { /* --- Tworzymy pasek i dodajemy go do okna --- */ pasek = gtk_toolbar_new (GTK_ORIENTATION_HORIZONTAL, GTK_TOOLBAR_ICONS); gtk_box_pack_start (GTK_BOX (ypole), pasek, FALSE, TRUE, 0); gtk_widget_show (pasek); /*
--------
* --- Tworzymy przycisk | nowy | *
--------
*/ gtk_toolbar_append_item (GTK_TOOLBAR (pasek), NULL, "Nowy plik", NULL, UtworzKontrolkeZXpm (glowne_okno, (gchar **) xpm_nowy), (GtkSignalFunc) menu_Nowy, NULL); /*
----------
* --- Tworzymy przycisk | otwórz | *
----------
*/ gtk_toolbar_append_item (GTK_TOOLBAR (pasek), "Otwórz plik ", "Otwórz plik", "", UtworzKontrolkeZXpm (glowne_okno, (gchar **) xpm_otworz), (GtkSignalFunc) menu_Otworz, NULL); /* --- Mały odstęp --- */ gtk_toolbar_append_space (GTK_TOOLBAR (pasek));
Część II Przykładowe aplikacje w GTK+
170 /*
----------
* --- Tworzymy przycisk | wytnij | *
----------
*/ gtk_toolbar_append_item (GTK_TOOLBAR (pasek), "Wytnij", "Wytnij", "", UtworzKontrolkeZXpm (glowne_okno, (gchar **) xpm_wytnij), (GtkSignalFunc) menu_Wytnij, NULL); /*
----------
* --- Tworzymy przycisk | kopiuj | *
----------
*/ gtk_toolbar_append_item (GTK_TOOLBAR (pasek), "Kopiuj", "Kopiuj", "", UtworzKontrolkeZXpm (glowne_okno, (gchar **) xpm_kopiuj), (GtkSignalFunc) menu_Kopiuj, NULL); /*
---------
* --- Tworzymy przycisk | wklej | *
---------
*/ gtk_toolbar_append_item (GTK_TOOLBAR (pasek), "Wklej", "Wklej", "", UtworzKontrolkeZXpm (glowne_okno, (gchar **) xpm_wklej), (GtkSignalFunc) menu_Wklej, NULL); }
rozne.c Plik ten zawiera funkcje niższego poziomu, wywoływane z funkcji UtworzMenu i UtworzPasekNarzedziowy. Zaczerpnięty jest z poprzedniego przykładu, ilustrującego użycie menu i pasków narzędziowych i nie ma w nim żadnych nowych elementów. Ponownie wykorzystujemy ten sam kod do tworzenia bitmap, menu i podmenu.
Tworzenie prostego edytora tekstu
171
komunikat.c W kilku częściach aplikacji konieczne jest wyświetlenie komunikatu dla użytkownika. Aby nie powielać tego samego kodu, napiszemy funkcję PokazKomunikat, która będzie wyświetlała okno dialogowe. Funkcja ta wywoła gtk_grab_add, aby upewnić się, że komunikat nie umknie uwadze użytkownika. Funkcja gtk_grab_add uniemożliwi użytkownikowi wykonanie jakiejkolwiek czynności, zanim nie zamknie on okna dialogowego. Po kliknięciu przycisku OK blokada zostanie zwolniona, dzięki wywołaniu funkcji gtk_grab_remove z funkcji zwrotnej dla sygnału destroy. Umieszczamy odpowiedni kod w funkcji zwrotnej dla sygnału destroy, ponieważ okno dialogowe może zostać zamknięte bez naciskania przycisku OK czy Anuluj. Funkcja PokazKomunikat przyjmuje tytuł i komunikat, nadaje tytuł oknu dialogowemu i umieszcza w nim komunikat oraz przycisk OK. /* * PokazKomunikat * * Wyświetla komunikat w oknie dialogowym */ void PokazKomunikat (char *szTytul, char *szKomunikat) { GtkWidget *etykieta; GtkWidget *przycisk; GtkWidget *okno_dialogowe; /* --- tworzymy okno dialogowe --- */ okno_dialogowe = gtk_dialog_new (); gtk_signal_connect (GTK_OBJECT (okno_dialogowe), "destroy", GTK_SIGNAL_FUNC (UsunOknoKomunikatu), NULL); /* --- ustawiamy tytuł i obramowanie --- */ gtk_window_set_title (GTK_WINDOW (okno_dialogowe), szTytul); gtk_container_border_width (GTK_CONTAINER (okno_dialogowe), 0); /* --- tworzymy przycisk OK z ogniskiem --- */ przycisk = gtk_button_new_with_label ("OK"); gtk_signal_connect (GTK_OBJECT (przycisk), "clicked", GTK_SIGNAL_FUNC (ZamknijOknoKomunikatu), okno_dialogowe);
Część II Przykładowe aplikacje w GTK+
172
/* --- Ustawiamy przycisk OK jako domyślny --- */ GTK_WIDGET_SET_FLAGS (przycisk, GTK_CAN_DEFAULT); gtk_box_pack_start (GTK_BOX ( GTK_DIALOG (okno_dialogowe)->action_area), przycisk, TRUE, TRUE, 0); gtk_widget_grab_default (przycisk); gtk_widget_show (przycisk); /* --- tworzymy etykietę na komunikat --- */ etykieta = gtk_label_new (szKomunikat); /* --- zostawiamy trochę miejsca wokół etykiety --- */ gtk_misc_set_padding (GTK_MISC (etykieta), 10, 10); /* --- Dodajemy etykietę do odpowiedniego --- */ /* --- obszaru okna dialogowego --- */ gtk_box_pack_start (GTK_BOX (GTK_DIALOG (okno_dialogowe)->vbox), etykieta, TRUE, TRUE, 0); /* --- Uwidaczniamy etykietę --- */ gtk_widget_show (etykieta); /* --- Wyświetlamy okno dialogowe --- */ gtk_widget_show (okno_dialogowe); /* --- Można wykonywać czynności tylko w tym oknie --- */ gtk_grab_add (okno_dialogowe); }
Aby funkcja PokazKomunikat działała bezbłędnie, konieczne jest obsłużenie dwóch sytuacji przy pomocy odpowiednich sygnałów. Użytkownik może kliknąć przycisk OK, ale może także po prostu zamknąć okno dialogowe. Kliknięcie przycisku powoduje wygenerowanie sygnału clicked, ale nie robi nic poza tym. Zamknięcie okna powoduje wygenerowanie sygnału destroy, ale nie bierze pod uwagę faktu, że wywołano funkcję gtk_grab_add. Aby można było zamknąć okno po kliknięciu przycisku OK, w momencie jego tworzenia przypisaliśmy do sygnału clicked funkcję ZamknijOknoKomunikatu. Funkcja ta musi wywołać funkcję gtk_grab_remove i usunąć okno dialogowe. Zauważmy, że kontrolka okna dialogowego jest przekazywana do tej funkcji jako parametr danych, co ustawiliśmy podczas konfigurowaniu sygnału clicked w gtk_signal_connect. /* * --- ZamknijOknoKomunikatu *
Tworzenie prostego edytora tekstu
173
* Procedura zamykająca okno dialogowe z komunikatem. */ void ZamknijOknoKomunikatu (GtkWidget *kontrolka, gpointer dane) { GtkWidget *kontrolka_dialogowa = (GtkWidget *) dane; /* --- usuwamy kontrolkę okna dialogowego --- */ gtk_widget_destroy (kontrolka_dialogowa); }
Jeśli użytkownik zamknie okno bez kliknięcia przycisku OK, wówczas okno zostanie usunięte z ekranu, ale dopiero po wysłaniu sygnału destroy. Funkcja UsunOknoKomunikatu ma przywrócić aplikację do stanu sprzed wyświetlenia okna dialogowego; tutaj wystarczy wywołanie funkcji gtk_grab_remove. /* * UsunOknoKomunikatu * * Zwalnia blokadę okna */ void UsunOknoKomunikatu (GtkWidget *kontrolka, gpointer dane) { gtk_grab_remove (kontrolka); }
oprogramie.c Możemy używać funkcji PokazKomunikat do tworzenia prostych okien dialogowych z wnętrza aplikacji. Na przykład opcja menu „O programie” powoduje wyświetlenie informacji o aplikacji w oknie dialogowym. Umieszcza się w nim zazwyczaj nazwę firmy, nazwisko autora i numer wersji programu. Cały kod dla opcji „O programie” składa się z wywołania funkcji PokazKomunikat, która wyświetli odpowiedni komunikat. /* * PokazOProgramie * * Pokazuje okno dialogowe "O programie". * Wykorzystuje napisany już kod */ void PokazOProgramie () {
Część II Przykładowe aplikacje w GTK+
174 PokazKomunikat ("O programie...", "Gnotatnik v.07\n - " "Eric Harlow\n"); }
wyborpliku.c Okno dialogowe wyboru plików jest wyświetlane przez procedury wczytujące i zapisujące pliki. Tytuł okna dialogowego jest przekazywany do funkcji PobierzNazwePliku, aby przypominał użytkownikowi o rodzaju wykonywanej operacji. Przekazywana jest także funkcja, aby okno wyboru pliku miało uniwersalne zastosowanie. Jeśli użytkownik kliknie przycisk OK, wówczas funkcja ta zostanie wywołana wraz z wybraną nazwą pliku. Technika ta umożliwia wykorzystanie tego samego kodu wyboru pliku do wczytywania i zapisywania plików. Podobnie jak poprzednio, funkcja gtk_grab_add uniemożliwia użytkownikowi wykonywanie innych czynności, zanim nie zamknie on okna dialogowego. Przechwytując sygnał destroy, możemy umieścić odpowiedzialny za czyszczenie kod w jednym miejscu. Kod wybierający plik korzysta ze struktury przydzielonej przez funkcję PobierzNazwePliku, która jest przekazywana do wszystkich funkcji zwrotnych, związanych z oknem dialogowym. Kod ten zawiera informacje o oknie dialogowym oraz o działaniu, które należy podjąć, jeśli użytkownik wciśnie przycisk OK. Struktura wygląda następująco: typedef struct { void (*funkcja) (gchar *); GtkWidget *wybpliku; } typDaneWyboruPliku; /* * PobierzNazwePliku * * Pokazuje okno wyboru pliku i jeśli użytkownik kliknie OK, wywołuje * funkcję wraz z nazwą wybranego pliku. */ void PobierzNazwePliku (char *sTytul, void (*fzwrotna) (char *)) { GtkWidget *kplik = NULL; typDaneWyboruPliku *dane;
Tworzenie prostego edytora tekstu
175
/* --- tworzymy nową kontrolkę wyboru pliku --- */ kplik = gtk_file_selection_new (sTytul); dane = g_malloc (sizeof (typDaneWyboruPliku)); dane->funkcja = fzwrotna; dane->wybpliku = kplik; gtk_signal_connect (GTK_OBJECT (kplik), "destroy", (GtkSignalFunc) usun, dane); /* --- podłączamy sygnał do przycisku OK --- */ gtk_signal_connect ( GTK_OBJECT (GTK_FILE_SELECTION (kplik)->ok_button), "clicked", (GtkSignalFunc) PlikOk, dane); /* --- podłączamy sygnał do przycisku Anuluj --- */ gtk_signal_connect_object ( GTK_OBJECT
(GTK_FILE_SELECTION
(kplik)-
>cancel_button), "clicked", (GtkSignalFunc) gtk_widget_destroy, (gpointer) kplik); if (sNazwaPliku) { /* --- ustawiamy domyślną nazwę pliku --- */ gtk_file_selection_set_filename (GTK_FILE_SELECTION (kplik), sNazwaPliku); } /* --- wyświetlamy okno dialogowe --- */ gtk_widget_show (kplik); /* --- przechwytujemy ognisko --- */ gtk_grab_add (kplik); }
Jeśli użytkownik kliknie OK, wówczas wywoływana jest funkcja przekazana do PobierzNazwePliku wraz z nazwą pliku z okna dialogowego. W przypadku naszej aplikacji będzie to funkcja służąca albo do otworzenia, albo do zapisania pliku. Kod korzysta także ze statycznej zmiennej (sNazwaPliku), w której przechowuje nazwę pliku. Jeśli użytkownik chce otworzyć plik, albo korzysta z opcji „Zapisz jako...”, aby zapisać plik, wówczas nazwa pliku jest umieszczana w oknie dialogowym, gdzie użytkownik będzie miał możliwość ją zmienić.
Część II Przykładowe aplikacje w GTK+
176
Funkcja PlikOK jest wywoływana po kliknięciu przycisku OK. Funkcja otrzymuje nazwę pliku z kontrolki, wywołuje kolejną funkcję, która wykona właściwą operację, a następnie usuwa okno dialogowe. Funkcja wykonująca operację jest tą samą, którą przekazaliśmy jako parametr funkcji PobierzNazwePliku. Funkcja jest przechowywana w globalnej zmiennej. Jest to możliwe, ponieważ w danym momencie może być widoczne tylko jedno okno dialogowe - gwarantuje to funkcja gtk_grab_add. Nie możemy otworzyć kolejnego okna wyboru pliku, nie zamykając uprzednio pierwszego. /* * PlikOK * * Kliknięto przycisk "OK" * Wywołujemy funkcję (funkcja) aby przeprowadzić żądaną * operację na pliku. */ void PlikOk (GtkWidget *k, gpointer dane) { char *sTmczNazwa; typDaneWyboruPliku *danelokalne; GtkWidget *kplik; danelokalne = (typDaneWyboruPliku *) data; kplik = danelokalne->filesel; /* --- Jaki plik? --- */ sTmczNazwa (kplik));
=
gtk_file_selection_get_filename
/* --- Zwalniamy niepotrzebną pamięć --- */ if (sNazwaPliku) g_free (sNazwaPliku); /* --- Powielamy łańcuch --- */ sNazwaPliku = g_strdup (sTmczNazwa); /* --- Wywołujemy funkcję, która przeprowadzi --- */ /* --- właściwą operację --- */ (*(danelokalne->funkcja)) (sNazwaPliku); /* --- Zamykamy okno dialogowe --- */ gtk_widget_destroy (kplik); }
(GTK_FILE_SELECTION
Tworzenie prostego edytora tekstu
177
Funkcja usun wywołuje gtk_grab_remove. Funkcję tego typu spotkamy w każdym programie, posługującym się oknami dialogowymi. /* * usun * * Funkcja obsługująca usuwanie okna dialogowego. Musimy zwolnić * przechwycone ognisko. */ static void usun (GtkWidget *kontrolka, gpointer *dane) { /* --- Zwalniamy ognisko --- */ gtk_grab_remove (kontrolka); g_free (dane); }
notatnik.c Funkcje w notatnik.c obsługują interakcje z kontrolką tekstu. Znajdują się tutaj funkcje służące do załadowania pliku do kontrolki i zapisania tekstu, oraz funkcje wycinające i wklejające tekst. Funkcja UtworzTekst jest wywoływana z main.c, aby utworzyć kontrolkę tekstu i skonfigurować ją do wykorzystania w edytorze. Kontrolka znajduje się w tablicy pakującej 2 x 2, wraz z poziomym i pionowym paskiem przewijania. Wskaźnik do kontrolki tekstu jest przechowywany w statycznej zmiennej, aby funkcje z tego modułu miały dostęp do kontrolki. Bezpośredni dostęp do kontrolki spoza tego modułu jest niedozowolony. /* * UtworzTekst * * Tworzy kontrolkę tekstu dla edytora * */ void UtworzTekst (GtkWidget *okno, GtkWidget *pojemnik) { GtkWidget *tabela; GtkWidget *xpasek; GtkWidget *ypasek; /* --- tworzymy tabelę, która będzie przechowywać kontrolkę
Część II Przykładowe aplikacje w GTK+
178 /* --- tekstu i paski przewijania --- */ tabela = gtk_table_new (2, 2, FALSE);
/* --- Dodajemy tabelę do pojemnika --- */ gtk_container_add (GTK_CONTAINER (pojemnik), tabela); /* --- Nie stosujemy odstępów, aby paski przewijania wyglądały jak część kontrolki tekstu --- */ gtk_table_set_row_spacing (GTK_TABLE (tabela), 0, 2); gtk_table_set_col_spacing (GTK_TABLE (tabela), 0, 2); /* --- uwidaczniamy tabelę --- */ gtk_widget_show (tabela); /* --- tworzymy kontrolkę tekstu --- */ tekst = gtk_text_new (NULL, NULL); /* --- pozwalamy na jej edycję --- */ gtk_text_set_editable (GTK_TEXT (tekst), TRUE); /* --- wstawiamy kontrolkę tekstu do tabeli --- */ gtk_table_attach (GTK_TABLE (tabela), tekst, 0, 1, 0, 1, GTK_EXPAND | GTK_SHRINK | GTK_FILL, GTK_EXPAND | GTK_SHRINK | GTK_FILL, 0, 0); /* --- uwidaczniamy ją --- */ gtk_widget_show (tekst); /* --- dodajemy poziomy pasek przewijania --- */ xpasek = gtk_hscrollbar_new (GTK_TEXT (tekst)->hadj); gtk_table_attach (GTK_TABLE (tabela), xpasek, 0, 1, 1, 2, GTK_EXPAND | GTK_FILL | GTK_SHRINK, GTK_FILL, 0, 0); gtk_widget_show (xpasek); /* --- dodajemy pionowy pasek przewijania --- */ ypasek = gtk_vscrollbar_new (GTK_TEXT (tekst)->vadj); gtk_table_attach (GTK_TABLE (tabela), ypasek, 1, 2, 0, 1, GTK_FILL, GTK_EXPAND | GTK_SHRINK | GTK_FILL, 0, 0); gtk_widget_show (ypasek); }
Tworzenie prostego edytora tekstu
179
Wycinanie, kopiowanie i wklejanie Kontrolka tekstu automatycznie obsługuje polecenia operujące na schowku, „Wytnij”, „Kopiuj” i „Wklej”, ale nie zmuszamy użytkownika, aby przeprowadzał te czynności za pomocą klawiszy (Ctrl+X, Ctrl+C, Ctrl+V). Opcje menu i przyciski paska narzędziowego powinny wydawać się równie zintegrowane, co skróty klawiszowe. Odwzorujemy je więc na funkcje, które przeprowadzają te same czynności, co polecenia wydawane z klawiatury. Na przykład opcja menu „Wytnij” będzie wywoływała funkcję gtk_editable_cut_clipboard, aby usunąć tekst z kontrolki i umieścić go w schowku. /* * WytnijTekst * * Wycina zaznaczony tekst z kontrolki i umieszcza go w schowku */ void WytnijTekst (GtkWidget *kontrolka, gpointer dane) { gtk_editable_cut_clipboard (GTK_EDITABLE (tekst)); } Polecenia „Kopiuj” i „Wklej” są równie łatwe w implementacji: /* * KopiujTekst * * Kopiuje zaznaczony tekst z kontrolki i umieszcza go w schowku */ void KopiujTekst (GtkWidget *kontrolka, gpointer dane) { gtk_editable_copy_clipboard (GTK_EDITABLE (tekst)); } /* * WklejTekst * * Wkleja tekst ze schowka do kontrolki */ void WklejTekst (GtkWidget *kontrolka, gpointer dane) { gtk_editable_paste_clipboard (GTK_EDITABLE (tekst)); }
Część II Przykładowe aplikacje w GTK+
180 Czyszczenie kontrolki
Edytor obsługuje już operacje wycinania, kopiowania i wklejania. Musi także wykonywać kilka innych poleceń; opcja menu Plik/Nowy powinna usunąć tekst z kontrolki. Operację tę również można przeprowadzić w jednym kroku: /* * WyczyscTekst * * Usuwa cały tekst z kontrolki */ void WyczyscTekst (GtkWidget *kontrolka, gpointer dane) { gtk_editable_delete_text (GTK_EDITABLE (tekst), 0, -1); }
Otwieranie plików Wczytywanie pliku do kontrolki wymaga skorzystania z okna wyboru pliku. Funkcja OtworzPlik jest przekazywana do funkcji PobierzNazwePliku i wywoływana wtedy, kiedy użytkownik kliknie przycisk OK. /* * menu_Otworz (main.c) * Wyświetlamy okno dialogowe, aby użytkownik mógł otworzyć plik */ void menu_Otworz (GtkWidget *kontrolka, gpointer dane) { PobierzNazwePliku ("Otwórz", OtworzPlik); }
Funkcja OtworzPlik wczytuje plik do kontrolki tekstu, wyświetlając stan operacji za pomocą paska postępów. Korzysta z trzech funkcji operujących na pasku postępu. Funkcja ZacznijPostep tworzy pasek postępów, UaktualnijPostep pokazuje aktualny postęp w ładowaniu pliku, a ZakonczPostep zamyka okno z paskiem postępów. Aby kontrolka nie traciła czasu na rysowanie ładowanego do niej tekstu, korzystamy z funkcji gtk_text_freeze. Plik jest wczytywany niewielkimi blokami, długości BUF_SIZE. Można ustawić tę stałą na większą wartość, aby zwiększyć szybkość wczytywania plików. Tutaj ustawiliśmy ją na małą wartość, ponieważ w innym przypadku pliki byłyby ładowane tak szybko, że pasek postępów nawet nie pojawiłby się na ekranie. /* * OtworzPlik
Tworzenie prostego edytora tekstu
* * sNazwaPliku - plik do wczytania * * Wczytuje plik i umieszcza go w kontrolce tekstu */ void OtworzPlik (char *sNazwaPliku) { char bufor[BUF_SIZE]; int lznakow; FILE *otw_plik; struct stat stanPliku; long dlugPliku = 0; /* --- Zamrażamy kontrolkę tekstu --- */ gtk_text_freeze (GTK_TEXT (tekst)); /* --- Opróżniamy kontrolkę --- */ gtk_editable_delete_text (GTK_EDITABLE (tekst), 0, -1); /* --- Pobieramy informację o pliku --- */ stat (sNazwaPliku, &stanPliku); dlugPliku = stanPliku.st_size; ZacznijPostep (); /* --- Otwieramy plik --- */ otw_plik = fopen (sNazwaPliku, "r"); /* --- Jeśli udało się otworzyć plik... --- */ if (otw_plik) { /* --- Odczytujemy blok--- */ while ((lznakow = fread (bufor, 1, BUF_SIZE, otw_plik)) > 0) { /* --- Uaktualniamy pasek postępów --- */ UaktualnijPostep (ftell (otw_plik), dlugPliku); /* --- Wstawiamy tekst --- */ gtk_text_insert (GTK_TEXT (tekst), NULL, NULL, NULL, bufor, lznakow); /* --- Czy to już koniec pliku? --- */ if (lznakow < BUF_SIZE) break;
181
Część II Przykładowe aplikacje w GTK+
182 } /* --- Zamykamy plik --- */ fclose (otw_plik); } ZakonczPostep ();
/* --- Odmrażamy kontrolkę tekstu - teraz się przerysuje --- */ gtk_text_thaw (GTK_TEXT (tekst)); }
Zapisywanie pliku Zapisywanie pliku przypomina jego wczytywanie, ale można zapisać cały plik jako jeden, duży blok pamięci. Blok ten, pobierany z kontrolki tekstu przy pomocy funkcji gtk_editable_get_chars, powinien zostać zwolniony funkcją g_free. /* * ZapiszPlik * * sNazwaPliku - nazwa zapisywanego pliku * * Funkcja zapisuje plik */ void ZapiszPlik (char *sNazwaPliku) { FILE *zap_plik; char *bufor; int lznakow; gtk_text_freeze (GTK_TEXT (tekst)); /* --- otwieramy plik --- */ zap_plik = fopen (sNazwaPliku, "w"); if (zap_plik) { /* --- pobieramy łańcuch z kontrolki --- */ bufor = gtk_editable_get_chars ( GTK_EDITABLE (tekst), (gint) 0, (gint) gtk_text_get_length (GTK_TEXT (tekst)));
Tworzenie prostego edytora tekstu
183
/* --- zapisujemy bufor na dysku --- */ lznakow = fwrite (bufor, sizeof (char), strlen (bufor), zap_plik); /* --- zamykamy plik --- */ fclose (zap_plik); if (lznakow != strlen (bufor)) { PokazKomunikat ("Zapisz", "Błąd: Nie można zapisać pliku."); } /* --- Zwalniamy pamięć --- */ g_free (bufor); } else { PokazKomunikat ("Zapisz", "Błąd: Nie można zapisać pliku."); } gtk_text_thaw (GTK_TEXT (tekst)); }
Szukanie tekstu Funkcję wyszukiwania zaimplementujemy za pomocą niemodalnego okna dialogowego. Okno „O programie” i okno wyboru pliku nie pozwalają na dostęp do aplikacji, dopóki są otwarte, natomiast okno wyszukiwania pozwala na otwieranie innych okien. Jednakże w danym momencie można korzystać tylko z jednej kopii tego okna. Opcja menu „Znajdź” jest odwzorowana na funkcję menu_Znajdz, która tworzy okno dialogowe i pozwala użytkownikowi na szukanie łańcucha, od bieżącej pozycji kursora aż do końca pliku. Dwie funkcje związane z oknem dialogowym odpowiadają jego dwóm przyciskom. Jeśli użytkownik kliknie przycisk Znajdź następny, wówczas wywoływana jest funkcja FunkcjaZnajdz, która szuka w kontrolce określonego łańcucha. Jeśli zaś użytkownik kliknie przycisk Anuluj, wówczas wywoływana jest funkcja FunkcjaAnuluj, która zamyka okno dialogowe. /* * menu_Znajdz *
Część II Przykładowe aplikacje w GTK+
184 * Znajduje łańcuch w edytorze */
void menu_Znajdz (GtkWidget *kontrolka, gpointer dane) { OknoDialogoweZnajdz ("Znajdź", FunkcjaZnajdz, FunkcjaAnuluj); }
Funkcja OknoDialogoweZnajdz tworzy okno dialogowe i przypisuje funkcje FunkcjaZnajdz i FunkcjaAnuluj do przycisków Znajdź następny i Anuluj. Funkcja sprawdza globalną zmienną (okno_dialogowe), zanim utworzy okno dialogowe, aby nie otwierać drugiej kopii okna. /* * OknoDialogoweZnajdz * * Funkcja wyświetlająca okno dialogowe "Znajdź" */ void OknoDialogoweZnajdz (char *szKomunikat, void (*FunkcjaTak)(), void (*FunkcjaNie)()) { GtkWidget *etykieta; GtkWidget *przycisk; GtkWidget *xpole; /* --- wracamy, jeśli okno dialogowe jest już otwarte --- */ if (okno_dialogowe) return; /* --- tworzymy okno dialogowe --- */ okno_dialogowe = gtk_dialog_new (); gtk_signal_connect (GTK_OBJECT (okno_dialogowe), "destroy", GTK_SIGNAL_FUNC (ZamknijOknoZnajdz), okno_dialogowe); /* --- ustawiamy tytuł --- */ gtk_window_set_title (GTK_WINDOW (okno_dialogowe), "Znajdź"); /* --- Dodajemy niewielkie obramowanie --- */ gtk_container_border_width (GTK_CONTAINER (okno_dialogowe), 5); /* * --- tworzymy komunikat */
Tworzenie prostego edytora tekstu
xpole = gtk_hbox_new (TRUE, TRUE); /* --- tworzymy etykietę z komunikatem --- */ etykieta = gtk_label_new ("Znajdź:"); gtk_widget_show (etykieta); /* --- tworzymy pole wpisu --- */ wpis = gtk_entry_new (); gtk_widget_show (wpis); /* --- Jeśli już czegoś szukaliśmy... --- */ if (szIgla) { /* --- Ustawiamy wpis na ostatnio szukany łańcuch --- */ gtk_entry_set_text (GTK_ENTRY (wpis), szIgla); } gtk_box_pack_start (GTK_BOX (xpole), etykieta, TRUE, TRUE, 0); gtk_box_pack_start (GTK_BOX (xpole), wpis, TRUE, TRUE, 0); gtk_widget_show (xpole); /* --- Dodajemy pole pakujące do pola dialogowego --- */ gtk_box_pack_start (GTK_BOX (GTK_DIALOG (okno_dialogowe)->vbox), xpole, TRUE, TRUE, 0); /* --- Tworzymy przycisk "Znajdź następny" --- */ przycisk = gtk_button_new_with_label ("Znajdź następny"); gtk_signal_connect (GTK_OBJECT (przycisk), "clicked", GTK_SIGNAL_FUNC (FunkcjaTak), okno_dialogowe); /* --- Dodajemy przycisk do okna dialogowego --- */ gtk_box_pack_start ( GTK_BOX (GTK_DIALOG (okno_dialogowe)->action_area), przycisk, TRUE, TRUE, 0); /* --- Uwidaczniamy przycisk --- */ gtk_widget_show (przycisk); /*
185
Część II Przykładowe aplikacje w GTK+
186 * --- Przycisk "Anuluj" */ /* --- tworzymy przycisk "Anuluj" --- */
przycisk = gtk_button_new_with_label ("Anuluj"); gtk_signal_connect (GTK_OBJECT (przycisk), "clicked", GTK_SIGNAL_FUNC (FunkcjaNie), okno_dialogowe); /* --- Pozwalamy, aby przycisk "Anuluj" był domyślnym --- */ GTK_WIDGET_SET_FLAGS (przycisk, GTK_CAN_DEFAULT); /* --- Dodajemy przycisk "Anuluj" do okna dialogowego --- */ gtk_box_pack_start ( GTK_BOX (GTK_DIALOG (okno_dialogowe)->action_area), przycisk, TRUE, TRUE, 0); /* --- Czynimy przycisk "Anuluj" domyślnym --- */ gtk_widget_grab_default (przycisk); /* --- Uwidaczniamy przycisk --- */ gtk_widget_show (przycisk); /* --- Wyświetlamy okno dialogowe --- */ gtk_widget_show (okno_dialogowe); }
Funkcja FunkcjaAnuluj usuwa okno i zeruje globalną zmienną (okno_ dialogowe), aby można było ponownie otworzyć okno. /* * FunkcjaAnuluj * * Zamyka okno dialogowe "Znajdź" */ void FunkcjaAnuluj (GtkWidget *kontrolka, gpointer dane) { /* --- Zamykamy okno --- */ gtk_widget_destroy (GTK_WIDGET (dane)); okno_dialogowe = NULL; }
Tworzenie prostego edytora tekstu
187
Zamiast tworzyć i usuwać okno dialogowe za każdym razem, kiedy użytkownik zechce z niego skorzystać, możemy utworzyć je tylko raz i uwidaczniać je w razie potrzeby. Kiedy użytkownik zakończy wyszukiwanie, okno jest ukrywane, co wygląda dokładnie tak, jakby zostało zamknięte. Metoda taka zużywa więcej pamięci w czasie pracy aplikacji, ale okno dialogowe jest wyświetlane szybciej, ponieważ nie musi być tworzone od nowa. Funkcja FunkcjaZnajdz szuka łańcucha w pliku. Wyszukiwanie rozpoczyna się od bieżącej pozycji kursora. Zamiast korzystać z funkcji gtk_text_get_point, aby pobrać pozycję kursora, użyjemy pola selection_end_pos w strukturze kontrolki. Robimy tak dlatego, że po przeprowadzeniu wyszukiwania znaleziony tekst zostanie podświetlony, a wyszukiwanie następnego wystąpienia tekstu musi rozpocząć się od końca zaznaczonego obszaru. Po odnalezieniu szukanego łańcucha musimy posłużyć się pewnym trikiem, aby kontrolka tekstu przewinęła się do zaznaczonego obszaru (samo zaznaczenie tekstu nie spowoduje jego ukazania się w kontrolce). Na szczęście wstawienie znaku do kontrolki powoduje przewinięcie wyświetlanego tekstu do punktu wstawienia. Ponieważ chcemy po prostu znaleźć łańcuch, możemy wstawić znak, pozwolić kontrolce na przewinięcie się do właściwej pozycji, a następnie usunąć znak. /* * FunkcjaZnajdz * * Szuka łańcucha w notatniku */ void FunkcjaZnajdz (GtkWidget *kontrolka, gpointer dane) { int nIndeks; GtkWidget *tekst = PobierzKontrolkeTekstu (); char *szStogSiana; /* --- Pobieramy tekst z kontrolki --- */ szStogSiana = PobierzTekst (); /* --- Zwalniamy poprzednią "igłę" (tekst) --- */ if (szIgla) { g_free (szIgla); } /* --- Pobieramy tekst, który należy znaleźć --- */ szIgla = gtk_editable_get_chars ( GTK_EDITABLE (wpis), 0, -1); /* --- Pobieramy pozycję kursora --- */ nIndeks = GTK_EDITABLE (tekst)->selection_end_pos;
Część II Przykładowe aplikacje w GTK+
188 /* --- Szukamy tekstu --- */
nIndeks = SzukajLancucha (szStogSiana, szIgla, nIndeks); if (nIndeks >= 0) { /* --- Przesuwamy kursor do właściwej pozycji --- */ gtk_text_set_point (GTK_TEXT (tekst), nIndeks); /* --- Te dwie linie powodują przewinięcie kontrolki do --- */ /*
miejsca, w którym znajduje się znaleziony tekst --- */
gtk_text_insert (GTK_TEXT (tekst), NULL, NULL, NULL, " ", 1); gtk_text_backward_delete (GTK_TEXT (tekst), 1); /* --- Zaznaczamy znaleziony tekst --- */ gtk_editable_select_region (GTK_EDITABLE (tekst), nIndeks, nIndeks + strlen (szIgla)); /* --- Pozwalamy ma ponowne utworzenie okna --- */ okno_dialogowe = NULL; } else { PokazKomunikat ("Znajdź...", "Nie znaleziono tekstu w pliku"); } /* --- Zwalniamy pamięć --- */ g_free (szStogSiana); }
Nie zapominajmy, że musimy zwolnić pamięć, zajmowaną przez łańcuch zwrócony z funkcji PobierzTekst, ponieważ funkcja ta wywołuje funkcję gtk_editable_get_chars, która zwraca blok pamięci przydzielony łańcuchowi. Funkcja SzukajLancucha przeprowadza „siłowe” wyszukiwanie łańcucha, zaczynając od przekazanej jej pozycji, i zwraca indeks pozycji odnalezionego tekstu albo -1, jeśli nie odnajdzie tekstu w kontrolce. /* * SzukajLancucha * * Szukamy łańcucha (szIgla) w dłuższym łańcuchu * (szStogSiana) zaczynając od określonej pozycji (nStart) * w dłuższym łańcuchu. * * Algorytm ten znany jest jako "algorytm siłowy" */
Tworzenie prostego edytora tekstu
int SzukajLancucha (char *szStogSiana, char *szIgla, int nStart) { int nDlugoscStoguSiana; int nDlugoscIgly; int nPoz; /* --- ustalamy długość łańcuchów --- */ nDlugoscStoguSiana = strlen (szStogSiana); nDlugoscIgly = strlen (szIgla); /* --- sprawdzamy każdy łańcuch --- */ for (nPoz = nStart; nPoz < nDlugoscStoguSiana; nPoz++) { /* --- Czy znaleźliśmy go w tym miejscu? --- */ if (strncmp (&szStogSiana[nPoz], szIgla, nDlugoscIgly) == 0) { /* --- tak, zwracamy indeks --- */ return (nPoz); } } /* Nie znaleźliśmy łańcucha, zwracamy -1 --- */ return (-1); } /* * PobierzTekst * * Zwraca tekst, znajdujący się w kontrolce */ char *PobierzTekst () { char *bufor; /* --- pobieramy łańcuch z kontrolki --- */ bufor = gtk_editable_get_chars ( GTK_EDITABLE (tekst), (gint) 0, (gint) gtk_text_get_length (GTK_TEXT (tekst))); return (bufor); }
189
Część II Przykładowe aplikacje w GTK+
190 /* * PobierzKontrolkeTekstu *
* Zwraca kontrolkę, która służy do wyświetlania tekstu. Funkcja * ta po prostu kapsułkuje globalną zmienną. */ GtkWidget *PobierzKontrolkeTekstu () { return (tekst); }
Pasek postępów Funkcje paska postępów omówiliśmy w rozdziale 6, „Dalsze kontrolki: ramki, tekst, okna dialogowe, okno wyboru pliku, pasek postępów”, a tutaj korzystamy z nich ponownie. Paska postępów używamy po to, aby poinformować użytkownika o stanie operacji, która może trwać przez dłuższy czas. Wczytywanie pliku jest celowo opóźniane, ponieważ w pętli korzystamy z niewielkiego bufora. Jeśli wczytywanie odbywałoby się zbyt szybko, pasek w ogóle nie zostałby wyświetlony. Możemy oczywiście zmienić rozmiar bufora, aby przyspieszyć wczytywanie pliku.
Podsumowanie GTK+ jest doskonałym narzędziem do szybkiego tworzenia aplikacji. W programie edytora, który napisaliśmy w tym rozdziale, większość pracy wykonywana jest przez kontrolki GTK+; wystarczyło połączyć ze sobą kontrolki menu, paska narzędziowego i tekstu, aby otrzymać gotową aplikację.
Rozdział 9 Saper Saper jest prostą grą, która zyskała popularność, kiedy Microsoft dołączył ją do systemu Windows. Celem gry jest odsłonięcie wszystkich znajdujących się na planszy pól, pod którymi nie ma bomb. Pierwsze próby są dość przypadkowe, ale odsłonięte kwadraty dostarczają pewnych wskazówek, które pomagają w rozbrojeniu reszty pól. Każde pole „wie”, ile bomb znajduje się w otaczających je polach, więc odkrycie pola bez bomby pozwala wywnioskować, którzy z jego sąsiadów mogą zawierać śmiercionośny ładunek. Aby zabawa była bardziej interesująca, zegar pokazuje nam, ile czasu zajęła gra, więc po wyczyszczeniu planszy możemy spróbować ukończyć grę w krótszym czasie. Planszę Sapera można obejrzeć na rysunku 9.1.
Rysunek 9.1. Saper.
Program Sapera uwypukla zagadnienia, którymi zajmowaliśmy się do tej pory. Prawdę powiedziawszy, szkielet programu bardzo przypomina aplikację kalkulatora. Oba programy wykorzystują przyciski w tabeli pakującej, ale Saper jest nieco bardziej skomplikowany. Posiada menu,
194
Część II Przykładowe aplikacje w GTK+
zawierające opcje ustawiające poziom trudności (początkujący, średni, zaawansowany) oraz pasek narzędziowy, który wyświetla zegar i liczbę nie odkrytych bomb. Saper wykorzystuje także rysunki xpm przedstawiające bomby, uśmiechnięty przycisk, kolorowe cyfry i flagi. Wszystkie przyciski przechowywane są w dwuwymiarowej tablicy elementów typu typPrzyciskSapera. Struktura ta przechowuje wskaźnik do kontrolki przycisku oraz informacje o tym, czy pod przyciskiem jest bomba i ile sąsiadów przycisku ukrywa bomby. /* * --- struktura danych, przechowująca przyciski Sapera */ typedef struct { int stanPrzycisku; /* bieżący stan przycisku */ GtkWidget *kontrolka; /* uchwyt do przycisku */ int nBombyWPoblizu; /* ile bomb jest wokół pola? */ int bMaBombe; /* czy pod przyciskiem jest bomba? */ int nWiersz; /* rząd tabeli pakującej */ int nKol; /* kolumna tabeli pakującej */ } typPrzyciskSapera;
Kiedy użytkownik rozpocznie grę, komputer szybko generuje planszę, rozmieszczając właściwą liczbę bomb pod niektórymi przyciskami. Oblicza także, ile bomb przylega do każdego pola planszy. Informacja ta jest początkowo ukrywana przed użytkownikiem, ale po kliknięciu na przycisku, pod którym nie ma bomby, jest ona wyświetlana na przycisku. Najpierw trzeba stworzyć dane dla rysunków. Rysunki są proste i intuicyjne. Składają się z kilku uśmiechniętych twarzy, które zostaną umieszczone na przycisku paska narzędziowego, flagi, przy pomocy której użytkownik może zaznaczać położenie bomb, oraz kolorowych cyfr, które wyświetlają liczbę bomb wokół danego pola.
bitmapy.h Rysunki wykorzystywane przez Sapera (oprócz kolorowych cyfr) znajdują się w pliku bitmaps.h. Zwróćmy uwagę, że twarz ma trzy różne rysunki - jeden wyświetlany w czasie gry, jeden w razie przegranej, a trzeci w przypadku zwycięstwa. Nie wymagają zbyt wiele kodu, a dodają grze trochę charakteru. /* * --- Flaga do zaznaczania bomb
Saper
*/ static char *xpm_flaga[] = { "12 12 4 1", " c None", "X c #000000", "R c #FF0000", "r c #AA0000", " ", " RRRRRRR ", " RRRRR r r ", " RRR r r r r ", " R r r r r r r ", " X ", " X ", " X ", " X ", " X ", " XXX ", " ", }; /* * --- Bomba. Cóż, każdemu trafia się gorszy dzień. */ static char *xpm_bomba[] = { "12 12 4 1", " c None", "X c #000000", "R c #FF0000", "r c #AA0000", " ", " X ", " X X X ", " XXXXX ", " XXXXX ", " XXXXXXXXX ", " XXXXX ", " XXXXX ", " X X X ", " X ", " ",
195
Część II Przykładowe aplikacje w GTK+
196 " };
",
/* * --- Zły ruch! */ static char *xpm_duzex[] = { "12 12 4 1", " c None", "X c #000000", "R c #FF0000", "r c #AA0000", "RRR RRR", " RRR RRR ", " RRR RRR ", " RRRRRR ", " RRRR ", " RRRR ", " RRRR ", " RRRRRR ", " RRR RRR ", " RRR RRR ", "RRR RRR", " ", }; /* * --- Bitmapa z uśmiechem */ static char *xpm_usmiech[] = { "16 16 4 1", " c None", ". c #000000", "X c #FFFF00", "r c #AA0000", " ...... ", " ..XXXXXX.. ", " ..XXXXXXXXXX. ", " .XXXXXXXXXXXX. ", " .XX..XXXX..XX. ", ".XXX..XXXX..XXX.",
Saper
".XXXXXXXXXXXXXX.", ".XXXXXXXXXXXXXX.", ".XXXXXXXXXXXXXX.", ".XXXXXXXXXXXXXX.", " .XX.XXXXXX.XX. ", " .XXX......XXX. ", " .XXXXXXXXXX. ", " ..XXXXXX.. ", " ...... ", " ", }; /* * --- Smutna mina. Przegrałeś. */ static char *xpm_smutek[] = { "16 16 4 1", " c None", ". c #000000", "X c #FFFF00", "r c #AA0000", " ...... ", " ..XXXXXX.. ", " ..XXXXXXXXXX. ", " .XXXXXXXXXXXX. ", " .XX.X.XX.X.XX. ", ".XXXX.XXXX.XXXX.", ".XXX.X.XX.X.XXX.", ".XXXXXXXXXXXXXX.", ".XXXXXXXXXXXXXX.", ".XXXXXXXXXXXXXX.", " .XXX......XXX. ", " .XX.XXXXXX.XX. ", " .XXXXXXXXXX. ", " ..XXXXXX.. ", " ...... ", " ", }; /* * --- Mamy zwyciêzcê.
197
198
Część II Przykładowe aplikacje w GTK+
*/ static char *xpm_wygrana[] = { "16 16 4 1", " c None", ". c #000000", "X c #FFFF00", "r c #AA0000", " ...... ", " ..XXXXXX.. ", " ..XXXXXXXXXX. ", " .XXXXXXXXXXXX. ", " .XX...XX...XX. ", ".XX..........XX.", ".X.X...XX...X.X.", "..XXXXXXXXXXXX..", ".XXXXXXXXXXXXXX.", ".XXXXXXXXXXXXXX.", " .XX.XXXXXX.XX. ", " .XXX......XXX. ", " .XXXXXXXXXX. ", " ..XXXXXX.. ", " ...... ", " ", };
cyfry.h Cyfry, oznaczające liczbę bomb wokół danego pola, są piksmapami. Ponieważ wszystkie dane dla przycisków znajdują się w piksmapach, procedury umieszczające na przyciskach bomby, flagi czy cyfry można zawrzeć w pojedynczym fragmencie kodu - zmienia się tylko rysunek. /* 1 - jasnoniebieski 2 - zielony 3 - czerwony 4 - ciemnoniebieski 5 - brązowo-fioletowy */ static const char *xpm_jeden[] = {
Saper
"12 12 2 1", " c None", "X c #3333CC", " ", " XX ", " XXX ", " X XX ", " XX ", " XX ", " XX ", " XX ", " XX ", " XXXXXX ", " ", " ", }; static const char *xpm_dwa[] = { "12 12 2 1", " c None", "X c #009900", " ", " XXXXXX ", " X X ", " XX ", " XX ", " XX ", " XX ", " XX ", " XX ", " XXXXXXXX ", " ", " ", }; static const char *xpm_trzy[] = { "12 12 2 1", " c None", "X c #AA0000", " ", " XXXXX ",
199
200 " XX ", " XX ", " XXXXXX ", " XX ", " XX ", " XX ", " XX ", " XXXXXX ", " ", " ", }; static const char *xpm_cztery[] = { "12 12 2 1", " c None", "X c #000066", " ", " XX XX ", " XX XX ", " XX XX ", " XX XX ", " XXXXXXXX ", " XX ", " XX ", " XX ", " XX ", " ", " ", }; static const char *xpm_piec[] = { "12 12 2 1", " c None", "X c #992299", " ", " XXXXXXXX ", " XX ", " XX ", " XXXXXXX ", " XX ", " XX ",
Część II Przykładowe aplikacje w GTK+
Saper
" XX ", " XX XX ", " XXXXXXX ", " ", " ", }; static const char *xpm_szesc[] = { "12 12 2 1", " c None", "X c #550055", " ", " XXXXXX ", " XX ", " XX ", " XXXXXXX ", " XX XX ", " XX XX ", " XX XX ", " XX XX ", " XXXXXX ", " ", " ", }; static const char *xpm_siedem[] = { "12 12 2 1", " c None", "X c #550000", " ", " XXXXXXXX ", " XX ", " XX ", " XX ", " XX ", " XX ", " XX ", " XX ", " XX ", " ", " ",
201
202
Część II Przykładowe aplikacje w GTK+
}; static const char *xpm_osiem[] = { "12 12 2 1", " c None", "X c #441144", " ", " XXXXXX ", " XX XX ", " XX XX ", " XXXXXX ", " XX XX ", " XX XX ", " XX XX ", " XX XX ", " XXXXXX ", " ", " ", };
zegar.c Gra musi przechowywać i uaktualniać liczbę sekund, które upłynęły od kliknięcia przez użytkownika pierwszego przycisku na planszy. Funkcja StartZegara jest wywoływana po pierwszym kliknięciu przycisku - prawdę powiedziawszy, jest wywoływana po każdym kliknięciu przycisku, ale jeśli zegar jest już uruchomiony, funkcja ignoruje wywołanie. Funkcja StartZegara uruchamia funkcję zwrotną (FunkcjaZwrotnaZegara), która będzie wywoływana co sekundę w celu uaktualnienia i wyświetlenia bieżącego czasu. Natomiast funkcja KoniecZegara wywoływana jest wtedy, kiedy gracz trafi na bombę, wygra grę, albo rozpocznie ją od nowa, klikając na uśmiechniętym przycisku. /* * zegar.c * * Autor: Eric Harlow * * Procedury do uaktualniania liczby sekund */ #include
Saper
203
static int nSekundy = 0; static gint zegar = 0; static int bZegarPracuje = FALSE; void UaktualnijSekundy (int); /* * FunkcjaZwrotnaZegara * * Będzie wywoływana co sekundę, aby uaktualnić liczbę * sekund w polu zegara. */ gint FunkcjaZwrotnaZegara (gpointer dane) { /* --- Upłynęła kolejna sekunda --- */ nSekundy++; UaktualnijSekundy (nSekundy); } /* * StartZegara * * Rozpoczyna odliczanie, kiedy użytkownik kliknie * pierwszy przycisk. */ void StartZegara () { /* --- Jeśli zegar jeszcze nie pracuje... --- */ if (!bZegarPracuje) { /* --- ...zaczynamy od zera --- */ nSekundy = 0; /* --- Wywołujemy FunkcjęZwrotnąZegara co 1000ms --- */ zegar = gtk_timeout_add (1000, FunkcjaZwrotnaZegara, NULL); /* --- Zegar pracuje --- */ bZegarPracuje = TRUE; } }
Część II Przykładowe aplikacje w GTK+
204
/* * KoniecZegara * * Zatrzymuje zegar. Może gracz trafił na bombę. */ void KoniecZegara () { /* --- Jeśli zegar pracuje... --- */ if (bZegarPracuje) { /* --- ...zatrzymujemy zegar --- */ gtk_timeout_remove (zegar); /* --- Ustawiamy odpowiednio znacznik --- */ bZegarPracuje = FALSE; } }
saper.c Gra składa się z siatki N x M przycisków - przełączników (GtkToggleButton), reprezentowanych przez strukturę typPrzyciskSapera. Każdemu przyciskowi przypisujemy rząd (nWiersz) oraz kolumnę (nKol) w siatce, a także znacznik, który wskazuje, czy pod przyciskiem znajduje się bomba (bMaBombe). Oprócz tego struktura przechowuje wskaźnik do utworzonego przycisku oraz licznik bomb, znajdujących się dookoła przycisku (używany tylko wtedy, kiedy pod przełącznikiem nie ma bomby). Znacznik stanPrzycisku odzwierciedla jeden ze stanów, w których może znajdować się przycisk. Zaczynamy od stanu PRZYCISK_NIEZNANY, ale użytkownik może zmienić stan przycisku na PRZYCISK_FLAGA, klikając go prawym klawiszem myszy. Czynność ta powoduje umieszczenie na przycisku rysunku flagi. Stan PRZYCISK_WLACZONY oznacza, że przycisk został wciśnięty. Jeśli pod przyciskiem znajduje się bomba, stan ten doprowadzi do zakończenia gry. Ostatni stan (PRZYCISK_NIEPEWNY) pozwala użytkownikowi zaznaczyć pole jako wątpliwe. W takim przypadku na przycisku pojawia się znak zapytania. Ten stan przycisku nie jest chwilowo zaimplementowany, ale nietrudno go dodać; pozostawiamy to jako ćwiczenie dla czytelników. Odrobina rekurencji upraszcza kodowanie. Jeśli na przykład użytkownik kliknie przycisk bez bomby, dookoła którego również nie ma żadnych bomb, wówczas pobliskie pola są rekurencyjnie odkrywane tak, jakby
Saper
205
same zostały kliknięte. Jeśli któryś z nich także nie sąsiaduje z żadnymi bombami, odkrywane są wszystkie pola dookoła niego itd. Podczas tworzenia przycisków każdemu z nich przypisujemy tę samą funkcję zwrotną, wywoływaną w przypadku kliknięcia. Parametr danych dla funkcji zwrotnej stanowi struktura danych przycisku, pochodząca z tablicy struktur typPrzyciskSapera. Dane, które otrzymuje funkcja zwrotna, dotyczą tylko klikniętego przycisku, ale ponieważ zawierają one rząd i kolumnę przycisku, można na ich podstawie określić także jego sąsiadów. /* * Plik: saper.c * Autor: Eric Harlow * * Program przypomina Sapera z Windows */ #include #include #include #include "rozne.h" #include "bitmapy.h" #include "cyfry.h" void UstawIkonePrzyciskuStart (gchar **xpm_dane); /* * --- Ustalamy rozmiar przycisków */ #define SZEROKOSC_PRZYCISKU 18 #define WYSOKOSC_PRZYCISKU 19 /* * --- Każda z cyfr, pokazujących liczbę bomb jest kolorową * bitmapą. Poniższa tablica znacznie przyspiesza * wyszukiwanie cyfr. */ gpointer cyfry[] = { NULL, xpm_jeden, xpm_dwa, xpm_trzy, xpm_cztery,
Część II Przykładowe aplikacje w GTK+
206 xpm_piec, xpm_szesc, xpm_siedem, xpm_osiem };
/* * --- To są dostępne stany przycisków * * NIEZNANY - Przycisk pusty. Nie wiadomo, co się pod nim kryje * FLAGA - Użytkownik umieścił na przycisku flagę, podejrzewając * że pod nim jest bomba * NIEPEWNY - Stan nie zaimplementowany * WLACZONY - Przycisk został wciśnięty */ enum { PRZYCISK_NIEZNANY, PRZYCISK_FLAGA, PRZYCISK_NIEPEWNY, PRZYCISK_WLACZONY }; /* * --- struktura danych, przechowująca przyciski Sapera */ typedef struct { int stanPrzycisku; /* bieżący stan przycisku */ GtkWidget *kontrolka; /* uchwyt do przycisku */ int nBombyWPoblizu; /* ile bomb jest wokół pola? */ int bMaBombe; /* czy pod przyciskiem jest bomba? */ int nWiersz; /* rząd tabeli pakującej */ int nKol; /* kolumna tabeli pakującej */ } typPrzyciskSapera; /* * --- Domyślne wartości rozmiarów siatki * i liczby bomb. */ static int nLWierszy = 10; static int nLKolumn = 10; static int nLiczbaBomb = 10;
Saper
207
/* * --- Siatkę tworzy dwuwymiarowa tablica. To są * maksymalne rozmiary. */ #define MAKS_WIERSZY 35 #define MAKS_KOLUMN 35 /* * Zmienne globalne */ /* --- Przypisujemy tablicy maksymalne rozmiary. Właściwie powinno się * robić to dynamicznie, ale siatkę najłatwiej zaimplementować * jako zwykłą dwuwymiarową tablicę */ typPrzyciskSapera plansza[MAKS_KOLUMN][MAKS_WIERSZY]; /* --- Znaczniki używane przez grę --- */ int bKoniecGry = FALSE; /* --- Gra skończona? --- */ int bOdPoczatku = FALSE; /* --- Gra skończona? --- */ int nPozostaleBomby; /* --- Nie odkryte bomby --- */ GtkWidget *tabela = NULL; GtkWidget *przycisk_start; GtkWidget *etykieta_bomby; GtkWidget *etykieta_czas; GtkWidget *ypole;
/* --- Tabela z siatką przycisków --- */ /* --- Przycisk zaczynający grę --- */ /* --- Etykieta z liczbą bomb --- */ /* --- Etykieta z czasem --- */ /* --- Pole pakujące w głównym oknie - */
/* * --- Prototypy */ void PokazUkryteInformacje (typPrzyciskSapera *pole); void UtworzPrzyciskiSapera (GtkWidget *tabela, int k, int w, int znacznik); void FZwrotnaZwolnijPotomka(GtkWidget *kontrolka); /* * SprawdzWygrana * * Sprawdza, czy gracz wygrał. Wygrana polega na tym, że liczba nie * odkrytych pól jest równa liczbie bomb. */ void SprawdzWygrana () {
Część II Przykładowe aplikacje w GTK+
208 int i, j; int nBomby = 0;
/* --- Przechodzimy przez wszystkie pola --- */ for (i = 0; i < nLKolumn; i++) { for (j = 0; j < nLWierszy; j++) { /* --- Jeśli przycisk jest nie wciśnięty albo opatrzony flagą... --- */ if (plansza[i][j].stanPrzycisku == PRZYCISK_NIEZNANY || plansza[i][j].stanPrzycisku == PRZYCISK_FLAGA) { /* --- ...to może być pod nim bomba. --- */ nBomby ++; } } } /* --- Czy jest tyle bomb, ile nie odkrytych pól? --- */ if (nBomby == nLiczbaBomb) { /* --- Koniec gry. Mamy zwycięzcę. --- */ KoniecZegara (); UstawIkonePrzyciskuStart ((gchar **) xpm_wygrana); bKoniecGry = TRUE; } } /* * UmiescIkoneNaPrzycisku * * Zmienia ikonę na przycisku na jeden z rysunków xpm * * pole - kwadrat siatki * xpm - dane rysunku */ void UmiescIkoneNaPrzycisku (typPrzyciskSapera *pole, char *xpm[]) { GtkWidget *kontrolka; /* --- tworzymy kontrolkę z danych xpm --- */ kontrolka = UtworzKontrolkeZXpm (tabela, (gchar **) xpm);
Saper
209 /* --- Umieszczamy piksmapę na rysunku --- */ gtk_container_add (GTK_CONTAINER (pole->kontrolka), kontrolka); /* --- usuwamy odniesienie do rysunku, aby została usunięta wraz z przyciskiem --- */ gdk_pixmap_unref ((GdkPixmap *) kontrolka);
} /* * UaktualnijSekundy * * Uaktualnia licznik sekund na pasku narzędziowym * * nSekundy - ile sekund należy wyświetlić */ void UaktualnijSekundy (int nSekundy) { char bufor[44]; /* --- Zmieniamy etykietę, aby wskazywała bieżący czas --- */ sprintf (bufor, "Czas: %d", nSekundy); gtk_label_set (GTK_LABEL (etykieta_czas), bufor); } /* * PokazLiczbeBomb * * Pokazuje liczbę pozostałych bomb. */ void PokazLiczbeBomb () { char bufor[33]; /* --- Zostało XX bomb --- */ sprintf (bufor, "Bomby: %d", nPozostaleBomby); gtk_label_set (GTK_LABEL (etykieta_bomby), bufor); } /* * ZwolnijPotomka * * Zwalnia wszystkich potomków kontrolki * Wywoływana wtedy, kiedy na przycisku trzeba umieścić nowy
210
Część II Przykładowe aplikacje w GTK+
* rysunek. Stary rysunek jest usuwany. */ void ZwolnijPotomka (GtkWidget *kontrolka) { /* --- Zwalniamy potomków przycisku --- */ gtk_container_foreach ( GTK_CONTAINER (kontrolka), (GtkCallback) FZwrotnaZwolnijPotomka, NULL); } /* * delete_event * * Okno jest zamykane, trzeba zakończyć pętlę GTK * */ void delete_event (GtkWidget *kontrolka, gpointer *dane) { gtk_main_quit (); } /* * PokazBomby * * Kliknięto na bombie, więc trzeba pokazać, gdzie rzeczywiście były * bomby (przynajmniej te, których dotąd nie odkryto). Wyświetlamy * nie znalezione bomby oraz te, które niby zostały znalezione, ale * wcale nie były bombami. */ void PokazBomby (typPrzyciskSapera *trafionobombe) { int i, j; typPrzyciskSapera *pole; GtkWidget *kontrolka_x; /* --- Przeglądamy wszystkie pola --- */ for (i = 0; i < nLKolumn; i++) { for (j = 0; j < nLWierszy; j++) { /* --- Pobieramy strukturę danych --- */
Saper
211 pole = &plansza[i][j]; /* --- Jeśli jest tutaj przycisk, a pod nim --- */ /* --- jest bomba... --- */ if (pole->stanPrzycisku == PRZYCISK_NIEZNANY && pole->bMaBombe) { /* --- ...wyświetlamy bombę --- */ PokazUkryteInformacje (pole); /* --- Jeśli zaznaczono pole flagą, a nie ma * w nim bomby... --- */ } else if (pole->stanPrzycisku == PRZYCISK_FLAGA && !pole->bMaBombe) { /* --- ...usuwamy flagę... --- */ ZwolnijPotomka (pole->kontrolka); /* --- ...i pokazujemy w tym miejscu X --- */ UmiescIkoneNaPrzycisku (pole, xpm_duzex); } } }
} /* * OdkryjSasiedniePola * * Odkrywa wszystkie pola wokół danego pola. * * kol, wiersz - pozycja, wokół której należy odkryć pola * Odkrywa wszystkie pola wokół oznaczonego niżej przez X: * * |------|------|-----* | | | | * -------------------* | | X | | * -------------------* | | | | * |------|------|-----*/ void OdkryjSasiedniePola (int kol, int wiersz) {
Część II Przykładowe aplikacje w GTK+
212 int i, j;
/* --- Sprawdzamy kolumnę wstecz i kolumnę do przodu --- */ for (i = MAX (kol-1, 0); i kontrolka), TRUE); return; } /* --- Jeśli przycisk jest opatrzony flagą, to nic nie ujawniamy --- */ if (pole->stanPrzycisku == PRZYCISK_FLAGA) { /* --- Gracz twierdzi, że jest tu bomba - nie odkrywamy * więc pola, choćby nie mogło być w nim bomby. */ gtk_toggle_button_set_state (GTK_TOGGLE_BUTTON (pole->kontrolka), FALSE);
Saper
213 } else { /* --- Ustawiamy stan przycisku na WŁĄCZONY --- */ pole->stanPrzycisku = PRZYCISK_WLACZONY; gtk_toggle_button_set_state (GTK_TOGGLE_BUTTON (pole->kontrolka), TRUE); /* --- Jeśli w tym polu jest bomba --- */ if (pole->bMaBombe) { /* --- Pokazujemy bombę w polu --- */ UmiescIkoneNaPrzycisku (pole, xpm_bomba); /* --- Nie ma bomby, ale są w pobliżu --- */ } else if (pole->nBombyWPoblizu) { /* --- Pokazujemy liczbę sąsiadujących bomb --- */ UmiescIkoneNaPrzycisku(pole, cyfry[pole->nBombyWPoblizu]); } else { /* --- Hmm. Kliknięto pole bez bomby i bez --- */ /* sąsiadujących bomb. Odkrywamy wszystkie --- */ /* pola dookoła (być może lawinowo) --- */ OdkryjSasiedniePola (pole->nKol, pole->nWiersz); } }
} /* * NowaGra * * Zerujemy grę, aby można było zagrać od nowa. Ustawiamy licznik * bomb i wyświetlamy pustą planszę. */ void NowaGra (int nKolumnySiatki, int nWierszeSiatki, int nBomby, int bNowePrzyciski) { /* --- Ustawiamy liczbę bomb w siatce --- */ nLiczbaBomb = nBomby; /* --- Ustawiamy liczbę nie odkrytych bomb --- */ nPozostaleBomby = nBomby;
Część II Przykładowe aplikacje w GTK+
214
/* --- Tworzymy przyciski sapera. --- */ UtworzPrzyciskiSapera (tabela, nKolumnySiatki, nWierszeSiatki, bNowePrzyciski); /* --- Zatrzymujemy zegar --- */ KoniecZegara (); UaktualnijSekundy (0); UstawIkonePrzyciskuStart ((gchar **) xpm_usmiech); } /* * FZwrotnaZwolnijPotomka * * Zwalnia kontrolkę. */ void FZwrotnaZwolnijPotomka(GtkWidget *kontrolka) { gtk_widget_destroy (kontrolka); } /* * UstawIkonePrzyciskuStart * * Ustawia ikonę przycisku rozpoczynającego grę. * Zazwyczaj będzie to albo wesoła, albo smutna twarz. */ void UstawIkonePrzyciskuStart (gchar **xpm_dane) { GtkWidget *kontrolka; /* --- Tworzymy kontrolkę z xpm --- */ kontrolka = UtworzKontrolkeZXpm (przycisk_start, xpm_dane); /* --- Zwalniamy wszystkich potomków przycisku --- */ ZwolnijPotomka (przycisk_start); /* --- Dodajemy rysunek do przycisku --- */ gtk_container_add (GTK_CONTAINER (przycisk_start), kontrolka); } /* * kliknieto_start
Saper
215
* * Procedura obsługi zdarzenia, wywoływana po kliknięciu * przycisku rozpoczynającego grę. */ void kliknieto_start (GtkWidget *kontrolka, gpointer *dane) { UstawIkonePrzyciskuStart ((gchar **) xpm_usmiech); NowaGra (nLKolumn, nLWierszy, nLiczbaBomb, FALSE); } /* * kliknieto_plansze * * Procedura obsługi zdarzenia, wywoływana po kliknięciu * któregoś z przycisków na planszy. * * kontrolka - kliknięty przycisk. * dane - struktura przycisku. */ void kliknieto_plansze (GtkWidget *kontrolka, gpointer *dane) { typPrzyciskSapera *pole; GtkWidget *etykieta; pole = (typPrzyciskSapera *) dane; /* --- Jeśli gra jest skończona --- */ if (bKoniecGry) { /* --- Zostawiamy przycisk tak, jak był. --- */ gtk_toggle_button_set_state (GTK_TOGGLE_BUTTON (kontrolka), (pole->stanPrzycisku == PRZYCISK_WLACZONY)); return; } /* --- Jeśli gra jest zerowana --- */ if (bOdPoczatku) return; /* --- Zaczynamy odliczanie czasu --- */ StartZegara (); /* --- Jeśli kliknięto bombę... --- */ if (pole->bMaBombe) {
Część II Przykładowe aplikacje w GTK+
216 /* --- Koniec gry! --- */ bKoniecGry = TRUE;
/* --- Twarz się zasmuca. --- */ UstawIkonePrzyciskuStart ((gchar **) xpm_smutek); /* --- Wyświetlamy wszystkie nie odkryte bomby. --- */ KoniecZegara (); PokazBomby (pole); } else { /* --- tworzymy etykietę dla przycisku i wyświetlamy --- */ /* --- na niej liczbę sąsiadujących bomb. --- */ PokazUkryteInformacje (pole); SprawdzWygrana (); } } /* * kliknieto_przycisk * * Użytkownik mógł kliknąć planszę prawym klawiszem myszy. * W takim przypadku należy odpowiednio zmienić rysunek * na przycisku. */ void kliknieto_przycisk (GtkWidget *kontrolka, GdkEventButton *zdarzenie, gpointer *dane) { typPrzyciskSapera *pole; GtkWidget *kontrolkaPiksmapy; /* --- Ignorujemy zdarzenie, jeśli gra jest już skończona --- */ if (bKoniecGry) { return; } /* --- Które pole kliknięto? --- */ pole = (typPrzyciskSapera *) dane; /* --- Upewniamy się, że zdarzenie to kliknięcie przycisku --- */ if (zdarzenie->type == GDK_BUTTON_PRESS) { /* --- Czy był to prawy klawisz myszy? --- */
Saper
217 if (zdarzenie->przycisk == 3) { switch (pole->stanPrzycisku) { case PRZYCISK_NIEZNANY: /* --- Zwalniamy potomków przycisku --- */ ZwolnijPotomka (kontrolka); pole->stanPrzycisku = PRZYCISK_FLAGA; UmiescIkoneNaPrzycisku (pole, xpm_flaga); nPozostaleBomby --; break; case PRZYCISK_FLAGA: /* --- Zwalniamy potomków przycisku --- */ ZwolnijPotomka (kontrolka); pole->stanPrzycisku = PRZYCISK_NIEZNANY; nPozostaleBomby ++; break; } PokazLiczbeBomb (); SprawdzWygrana (); } }
} /* * UtworzPrzycisk * * Tworzy przycisk, przypisuje funkcje obsługi zdarzeń i umieszcza * go we właściwym miejscu tabeli pakującej */ GtkWidget *UtworzPrzycisk (GtkWidget *tabela, typPrzyciskSapera *pole, int wiersz, int kolumna) { GtkWidget *przycisk; /* --- tworzymy przycisk --- */ przycisk = gtk_toggle_button_new ();
Część II Przykładowe aplikacje w GTK+
218
/* --- Inicjujemy pola struktury przycisku --- */ pole->stanPrzycisku = PRZYCISK_NIEZNANY; pole->nWiersz = wiersz; pole->nKol = kolumna; /* --- Musimy sprawdzać, czy kliknięto przycisk --- */ gtk_signal_connect (GTK_OBJECT (przycisk), "clicked", GTK_SIGNAL_FUNC (kliknieto_plansze), pole); /* --- Istotne są także inne zdarzenia --- */ gtk_signal_connect (GTK_OBJECT (przycisk), "button_press_event", GTK_SIGNAL_FUNC (kliknieto_przycisk), pole); /* --- Umieszczamy przycisk we właściwej komórce tabeli --- */ gtk_table_attach (GTK_TABLE (tabela), przycisk, kolumna, kolumna + 1, wiersz + 1, wiersz + 2, GTK_FILL | GTK_EXPAND, GTK_FILL | GTK_EXPAND, 0, 0); /* --- Ustawiamy jednolity rozmiar przycisku --- */ gtk_widget_set_usize (przycisk, SZEROKOSC_PRZYCISKU, WYSOKOSC_PRZYCISKU); /* --- Uwidaczniamy przycisk --- */ gtk_widget_show (przycisk); /* --- zwracamy przycisk. --- */ return (przycisk); } /* * PoliczSasiednieBomby * * Oblicza, ile bomb znajduje się w sąsiednich polach */ int PoliczSasiednieBomby (int kol, int wiersz) { int i, j; int nLiczba = 0; /* --- Każde pole oddalone co najwyżej o 1 --- */
Saper
219 for (i = MAX (kol-1, 0); i stanPrzycisku = PRZYCISK_NIEZNANY; /* --- Kontrolka już umieszczona na planszy? --- */ if (bNowePrzyciski) { /* --- tworzymy nowy przycisk --- */ pole->kontrolka = UtworzPrzycisk(tabela, pole, wi, ki); } else { /* --- wykorzystujemy przycisk ponownie --- */ /* --- Zwalniamy istniejące rysunki xpm --- */ ZwolnijPotomka (pole->kontrolka); /* --- Wyłączamy przycisk --- */ gtk_toggle_button_set_state ( GTK_TOGGLE_BUTTON (pole->kontrolka), FALSE); } } } /* --- Rozmieszczamy bomby na planszy. --- */ nBomby = nLiczbaBomb; /* --- Dopóki mamy jeszcze bomby do rozmieszczenia... --- */ while (nBomby > 0) { /* --- Obliczamy pozycję rząd/kolumna --- */ ki = rand () % nLKolumn; wi = rand () % nLWierszy; /* --- Jeśli bomba jeszcze nie istnieje, tworzymy ją! --- */ if (plansza[ki][wi].bMaBombe == 0) { plansza[ki][wi].bMaBombe = 1; nBomby--; }
Saper
221 } /* --- Po rozmieszczeniu bomb obliczamy, ile bomb * przylega do każdego przycisku. */ /* --- Sprawdzamy każdy przycisk --- */ for (ki = 0; ki < nLKolumn; ki++) { for (wi = 0; wi < nLWierszy; wi++) { pole = &plansza[ki][wi]; /* --- Ile przycisków --- */ pole->nBombyWPoblizu = PoliczSasiednieBomby (ki, wi); } } bOdPoczatku = FALSE;
} /* * UstawSiatke * * Ustawia siatkę gry na określony rozmiar i liczbę bomb */ void UstawSiatke (int nKolumnySiatki, int nWierszeSiatki, int nBomby) { int wiersz, kol; /* --- Jeśli istnieje tabela pakująca... --- */ if (tabela) { /* --- ...usuwamy ją wraz z wszystkimi przyciskami --- */ gtk_widget_destroy (tabela); } /* --- Tworzymy tabelę na przyciski --- */ tabela = gtk_table_new (nKolumnySiatki, nWierszeSiatki, FALSE); /* --- Dodajemy ją do pionowego pola pakującego --- */ gtk_box_pack_start (GTK_BOX (ypole), tabela, FALSE, FALSE, 0); /* --- Realizujemy tabelę --- */ gtk_widget_realize (tabela);
Część II Przykładowe aplikacje w GTK+
222
/* --- Zerujemy grę, określając nowe wartości --- */ NowaGra (nKolumnySiatki, nWierszeSiatki, nBomby, TRUE); /* --- Uwidaczniamy tabelę --- */ gtk_widget_show (tabela); } /* * main * * Tutaj zaczyna się program */ int main (int argc, char *argv[]) { GtkWidget *okno; GdkBitmap *maska; GtkStyle *styl; GtkWidget *kontrolka_usmiech; GtkWidget *xpole; /* --- Inicjacja GTK --- */ gtk_init (&argc, &argv); /* --- Tworzymy okno najwyższego poziomu --- */ okno = gtk_window_new (GTK_WINDOW_TOPLEVEL); /* --- Nie pozwalamy na zmianę rozmiarów okna --- */ gtk_window_set_policy (GTK_WINDOW (okno), FALSE, FALSE, TRUE); /* --- Nadajemy oknu tytuł --- */ gtk_window_set_title (GTK_WINDOW (okno), "Saper"); /* --- Zawsze należy pamiętać o podłączeniu sygnału * "delete_event" do głównego okna */ gtk_signal_connect (GTK_OBJECT (okno), "delete_event", GTK_SIGNAL_FUNC (delete_event), NULL);
ypole = gtk_vbox_new (FALSE, 1); gtk_widget_show (ypole); /* --- tworzymy menu aplikacji --- */ UtworzMenu (okno, ypole);
Saper
223 /* --- Poziome pole na wyniki i przycisk start --- */ xpole = gtk_hbox_new (TRUE, 1); gtk_widget_show (xpole); gtk_box_pack_start (GTK_BOX (ypole), xpole, FALSE, FALSE, 0); /* * --- Nie odkryte bomby będziemy wyświetlać na etykiecie */ /* --- Dodajemy etykietę z liczbą bomb --- */ etykieta_bomby = gtk_label_new (""); gtk_box_pack_start (GTK_BOX (xpole), etykieta_bomby, FALSE, FALSE, 0); gtk_widget_show (etykieta_bomby); /* * --- Tworzymy przycisk startowy z uśmiechniętą twarzą */ przycisk_start = gtk_button_new (); /* --- Musimy wiedzieć, że przycisk został kliknięty --- */ gtk_signal_connect (GTK_OBJECT (przycisk_start), "clicked", GTK_SIGNAL_FUNC (kliknieto_start), NULL); gtk_box_pack_start (GTK_BOX (xpole), przycisk_start, FALSE, FALSE, 0); gtk_widget_show (przycisk_start); /* * --- Czas wyświetlamy po prawej */ /* --- Dodajemy etykietę z czasem --- */ etykieta_czas = gtk_label_new (""); gtk_box_pack_start (GTK_BOX (xpole), etykieta_czas, FALSE, FALSE, 0); gtk_widget_show (etykieta_czas); /* --- Uwidaczniamy pole --- */ gtk_widget_show (ypole);
Część II Przykładowe aplikacje w GTK+
224
/* --- Dodajemy pionowe pole do okna aplikacji --- */ gtk_container_add (GTK_CONTAINER (okno), ypole); gtk_widget_show (okno); /* --- Tworzymy "uśmiech" i umieszczamy go na przycisku --- */ UstawIkonePrzyciskuStart ((gchar **) xpm_usmiech); /* --- Tworzymy siatkę 10x10. --- */ UstawSiatke (10, 10, 10); /* --- Uruchamiamy pętlę zdarzeń GTK --- */ gtk_main (); exit (0); }
menu.c Funkcje w pliku menu.c obsługują tworzenie i wybór elementów menu. Interfejs do kodu sapera ograniczamy do niezbędnego minimum. /* * Interfejs GUI aplikacji Sapera. * * Autor: Eric Harlow * */ #include #include #include #include #include "rozne.h" void UstawSiatke (int nLKolumn, int nLWierszy, int nBomby); /* * --- Zmienne globalne */ GtkWidget *glowne_okno; GtkAccelGroup *grupa_skrotow; GtkWidget *pasek; /*
Saper
225
* menu_Nowy * * Wywoływana po wybraniu menu Gra * Tworzy nową grę */ void menu_Nowy (GtkWidget *kontrolka, gpointer dane) { /* --- Parametery nie są używane... --- */ /* --- Udajemy, że kliknięto przycisk startowy. --- */ kliknieto_start (NULL, NULL); } /* * funkcjaPoczatkujacy * * Wywoływana po wybraniu z menu opcji "Początkujący" * Tworzy małą siatkę */ void funkcjaPoczatkujacy (GtkWidget *kontrolka, gpointer dane) { if (GTK_CHECK_MENU_ITEM (kontrolka)->active) { UstawSiatke (10, 10, 10); } } /* * funkcjaSredni * * Wywoływana po wybraniu z menu opcji "Średni" * Tworzy średnią siatkę */ void funkcjaSredni (GtkWidget *kontrolka, gpointer dane) { if (GTK_CHECK_MENU_ITEM (kontrolka)->active) { UstawSiatke (20, 15, 40); } } /* * funkcjaZaawansowany *
Część II Przykładowe aplikacje w GTK+
226
* Wywoływana po wybraniu z menu opcji "Zaawansowany" * Tworzy największą siatkę, z największą liczbą bomb */ void funkcjaZaawansowany (GtkWidget *kontrolka, gpointer dane) { /* --- jeśli element menu jest obecnie aktywny --- */ if (GTK_CHECK_MENU_ITEM (kontrolka)->active) { /* --- Ustawiamy rozmiary siatki --- */ UstawSiatke (30, 20, 100); } } /* * menu_Zakoncz * * Wybrano opcję "Zakończ" * Zamykamy program */ void menu_Zakoncz (GtkWidget *kontrolka, gpointer dane) { gtk_main_quit (); } /* * menu_OProgramie * * Wybrano opcję menu "O programie". * Pokazujemy informacje o aplikacji. */ void menu_OProgramie (GtkWidget *kontrolka, gpointer dane) { PokazOProgramie (); } /* * UtworzGlowneOkno * * Tworzy główne okno i związane z nim menu/pasek. */ void UtworzMenu (GtkWidget *okno, GtkWidget *ypole)
Saper
227
{ GtkWidget *pasekmenu; GtkWidget *menu; GtkWidget *elmenu; GSList *grupa = NULL; glowne_okno = okno; /* --- tworzymy tablicę skrótów --- */ grupa_skrotow = gtk_accel_group_new (); gtk_accel_group_attach (grupa_skrotow, GTK_OBJECT (okno)); /* --- Pasek menu --- */ pasekmenu = gtk_menu_bar_new (); gtk_box_pack_start (GTK_BOX (ypole), pasekmenu, FALSE, TRUE, 0); gtk_widget_show (pasekmenu); /* --------------------- Menu Plik ------------------- */ menu = UtworzPodmenuPaska (pasekmenu, "Gra"); elmenu = UtworzElementMenu (menu, "Nowa", "^N", "Nowa gra", GTK_SIGNAL_FUNC (menu_Nowy), NULL); elmenu = UtworzElementMenu (menu, NULL, NULL, NULL, NULL, NULL); elmenu = UtworzOpcjonalnyElement (menu, "Początkujący", &grupa, GTK_SIGNAL_FUNC (funkcjaPoczatkujacy), NULL); elmenu = UtworzOpcjonalnyElement (menu, "Średni", &grupa, GTK_SIGNAL_FUNC (funkcjaSredni), NULL); elmenu = UtworzOpcjonalnyElement (menu, "Zaawansowany", &grupa, GTK_SIGNAL_FUNC (funkcjaZaawansowany), NULL); elmenu = UtworzElementMenu (menu, NULL, NULL, NULL, NULL, NULL); elmenu = UtworzElementMenu (menu, "Zakończ", "", "Czy jest bardziej wymowna opcja?", GTK_SIGNAL_FUNC (menu_Zakoncz), "zakończ");
Część II Przykładowe aplikacje w GTK+
228
/* ------------------------ Menu Pomoc ---------------------- */ menu = UtworzPodmenuPaska (pasekmenu, "Pomoc"); elmenu = UtworzElementMenu (menu, "O programie", NULL, "O Saperze...", GTK_SIGNAL_FUNC (menu_OProgramie),"o programie"); }
Pozostałe pliki Pozostałe pliki zostały już omówione; można je pobrać spod adresu www.mcp.com (patrz rozdział 1, „Wprowadzenie do GTK+”). Zawierają one funkcje pomocnicze, tworzące menu i okno dialogowe dla opcji „O programie”. Okna dialogowe opisaliśmy w rozdziale 6, „Dalsze kontrolki: ramki, tekst, okna dialogowe, okno wyboru pliku, pasek postępów”, natomiast funkcje tworzące menu opisaliśmy w rozdziale 5, „Menu, paski narzędziowe i podpowiedzi”.
Podsumowanie Napisanie programu Sapera powinno pogłębić naszą wiedzę o składaniu prostych aplikacji z pasków narzędziowych, menu i innych kontrolek. Biblioteka GTK+ umożliwia stworzenie wielu typów aplikacji z tych samych kontrolek.
Rozdział 10 GDK (Graphics Drawing Kit) GDK (Graphics Drawing Kit) jest niskopoziomową warstwą, znajdującą się pomiędzy GTK+ i zależnym od systemu operacyjnego interfejsem API (Application Programming Interface) - w przypadku Linuksa jest to Xlib. Ponieważ biblioteka GTK+ nie współpracuje bezpośrednio z interfejsem API komputera, jej przeniesienie do innego systemu jest kwestią przeniesienia GDK i GLIB. GDK udostępnia funkcje rysujące (aż do poziomu pojedynczych punktów), oraz niskopoziomowe funkcje tworzące okna i manipulujące nimi. Kontrolki GTK+ są wystarczające w wielu aplikacjach, ale jeśli zechcielibyśmy napisać program analogowego zegara, byłoby to trudne, gdybyśmy nie dysponowali możliwością narysowania tarczy zegara. Wykorzystanie kontrolki obszaru rysunkowego (drawing area widget) wraz z funkcjami GDK pozwoli nam na narysowanie dowolnych obiektów; nie jesteśmy więc skazani na gotowe kontrolki.
Funkcje rysujące Pisanie aplikacji z wykorzystaniem procedur GDK jest niewiele łatwiejsze, niż posługiwanie się bezpośrednio biblioteką Xlib. Na szczęście GTK+ zawiera kontrolkę, którą można użyć w aplikacjach wymagających samodzielnego rysowania. Kontrolką obszaru rysunkowego posługujemy się w ten sam sposób, co wszystkimi innymi kontrolkami GTK+; jest ona przy tym wystarczająco elastyczna, aby stać się podstawą aplikacji wymagających użycia grafiki. Zaletą takiego podejścia jest to, że w jednej aplikacji możemy użyć zarówno GTK+, jak i GDK. GTK+ zapewnia wówczas menu, paski narzędziowe i inne kontrolki, które wspierają rysowanie w kontrolce obszaru rysunkowego. GDK udostępnia interfejs API, służący do kreślenia linii, pikseli, prostokątów, okręgów i innych figur. Każda procedura GDK przyjmuje przynajmniej dwa parametry - obszar rysunkowy (GdkDrawable) oraz GdkDC. GdkDrawable reprezentuje obszar, na którym będzie odbywać się rysowanie, natomiast GdkDC zawiera informacje o czcionce i kolorze oraz inne parametry rysowania.
234
Część III Rysowanie, kolor i GDK
Rysowanie pikseli GDK udostępnia procedury służące do rysowania wewnątrz okien i kontrolek (takich jak obszar rysunkowy). Najprostszą z nich jest procedura gdk_draw_point, służąca do rysowania pojedynczego punktu wewnątrz obszaru rysunkowego. /* --- rysujemy punkt o współrzędnych 10, 10 --- */ gdk_draw_point (obszar_rys, gc, 10, 10);
W tym i w dalszych przykładach obszar_rys reprezentuje obszar, w którym odbywa się rysowanie, a gc zazwyczaj reprezentuje informacje o kolorze. W dalszej części tego rozdziału nauczymy się pobierać i ustawiać te parametry.
Rysowanie linii Funkcja gdk_draw_line służy do rysowania linii pomiędzy dwoma punktami. /* --- rysujemy ukośną linię --- */ gdk_draw_line (obszar_rys, gc, 0, 0, 10, 10);
Rysowanie prostokątów Funkcja gdk_draw_rectangle rysuje prostokąt wewnątrz obszaru rysunkowego. Prostokąt może zostać wypełniony kolorem - z możliwości tej często korzysta się, aby wymazać obszar ekranu, który wymaga przerysowania. Pierwsze dwie liczby są współrzędnymi lewego górnego rogu prostokąta, a dwie ostatnie liczby określają szerokość i wysokość prostokąta. Trzeci parametr określa, czy prostokąt należy wypełnić, czy też pozostawić go pustym. Wartość TRUE nakazuje wypełnienie prostokąta. /* --- rysujemy prostokąt na ekranie (0, 0), (20, 20) --- */ gdk_draw_rectangle (drawable, gc, FALSE, 0, 0, 20, 20); /* --- rysujemy wypełniony prostokąt (10, 10), (30, 30) --- */ gdk_draw_rectangle (drawable, gc, TRUE, 10, 10, 30, 30);
Rysowanie wielokątów Rysowanie bardziej skomplikowanych kształtów (na przykład ośmiokąta albo ludzkiej głowy) wymaga nakreślenia wielu linii. Zamiast wielokrot-
235 nie wywoływać funkcję gdk_draw_line, zazwyczaj wygodniej jest skorzystać z funkcji gdk_draw_polygon i przekazać do niej tablicę punktów, które należy połączyć liniami. Funkcja połączy także pierwszy i ostatni punkt w tablicy, jeśli jest to ten sam punkt. Można wypełnić wielokąt, ustawiając odpowiedni parametr na TRUE. Oto jeden ze sposobów narysowania trójkąta: GdkPoint punkty[4]; /* --- górny róg trójkąta --- */ punkty[0].x = 50; punkty[0].y = 0; /* --- dolny, lewy róg trójkąta --- */ punkty[0].x = 0; punkty[0].y = 50; /* --- dolny, prawy róg trójkąta --- */ punkty[0].x = 100; punkty[0].y = 50; /* --- zamykamy trojkąt --- */ punkty[0].x = 50; punkty[0].y = 0; /* --- rysujemy trójkąt (bez wypełnienia) --- */ gdk_draw_polygon (obszar_rys, gc, FALSE, punkty, 4); /* --- rysujemy trójkąt i wypełniamy go --- */ gdk_draw_polygon (obszar_rys, gc, TRUE, punkty, 4);
Rysowanie wielu linii Istnieją dwie funkcje, które mogą przyspieszyć rysowanie wielu linii. Funkcja gdk_draw_lines przyjmuje listę punktów i łączy je liniami. Funkcja gdk_draw_segments przyjmuje tablicę odcinków i rysuje je jako niezależne linie. W poniższym przykładzie używamy obu funkcji, aby narysować ten sam trójkąt, który rysowaliśmy wcześniej. /* * najpierw do narysowania trójkąta użyjemy funkcji gdk_draw_lines */ /* --- górny róg trójkąta --- */
236
Część III Rysowanie, kolor i GDK
punkty[0].x = 50; punkty[0].y = 0; /* --- dolny, lewy róg trójkąta --- */ punkty[0].x = 0; punkty[0].y = 50; /* --- dolny, prawy róg trójkąta --- */ punkty[0].x = 100; punkty[0].y = 50; /* --- zamykamy trójkąt --- */ punkty[0].x = 50; punkty[0].y = 0; /* --- rysujemy linie pomiędzy czterema punktami --- */ gdk_draw_lines (obszar_rys, gc, punkty, 4); /* * teraz narysujemy trójkąt za pomocą funkcji gdk_draw_segments */ /* --- określamy odcinki trójkąta --- */ odcinek[0].x1 = 50; odcinek[0].y1 = 0; odcinek[1].x2 = 0; odcinek[1].y2 = 50; odcinek[2].x1 = 0; odcinek[2].y1 = 50; odcinek[3].x2 = 100; odcinek[3].y2 = 50; odcinek[4].x1 = 100; odcinek[4].y1 = 50; odcinek[5].x2 = 50; odcinek[5].y2 = 0; /* --- rysujemy trzy odcinki --- */ gdk_draw_segments (obszar_rys, gc, odcinek, 3);
Rysunek 10.1 przedstawia różnicę pomiędzy rysowaniem wielu linii i wielu odcinków.
237
Rysunek 10.1. Rysowanie wielu linii i odcinków.
Można także użyć funkcji gdk_draw_points, aby jednocześnie narysować wiele punktów wewnątrz kontrolki obszaru rysunkowego. Jest to szybsze, niż wielokrotne wywoływanie funkcji gdk_draw_point, ale niezależnie od użytej metody, rysowanie przez aplikację pojedynczych punktów jest dosyć wolne. Jeśli to możliwe, należy wykorzystać piksmapy.
Rysowanie okręgów i łuków Funkcja gdk_draw_arc rysuje łuk albo okrąg wewnątrz obszaru rysunkowego. Funkcja ta wymaga bardziej szczegółowego objaśnienia, niż pozostałe funkcje, ponieważ przyjmuje znacznie więcej parametrów. Prototyp funkcji jest zdefiniowany następująco: gdk_draw_arc (GdkDrawable *obszar_rys, /* - obszar rysunkowy - */ gint *gc, /* - dane (kolor/czcionka) - */ gint wypelniony, /* - czy należy wypełniać? - */ gint x, /* - lewa strona - */ gint y, /* - górna strona - */ gint szerokosc, gint wysokosc, gint kat1, /* - kąt początkowy - */ gint kat2); /* - kąt końcowy - */
Definiowanie kąta początkowego i końcowego łuku wprowadza trochę zamieszania, ponieważ w przeciwieństwie do większości funkcji operujących na łukach, nie określa się ich w stopniach ani radianach. Kąt należy przekształcić na stopnie i pomnożyć przez 64. Kąt 0 znajduje się na godzinie trzeciej, a wzrasta przeciwnie do ruchu wskazówek zegara. Jeśli
Część III Rysowanie, kolor i GDK
238
chcielibyśmy narysować łuk od szczytu do spodu prostokąta, rysowalibyśmy go od kąta (90 * 64) do (180 * 64). Rysunek 10.2 ilustruje te rozważania.
Rysunek 10.2. gdk_draw_arc. /* --- rysujemy pełny okrąg --- */ gdk_draw_arc (obszar_rys, gc, FALSE, 0, 0, 30, 30, 0, (360 * 64)); /* --- rysujemy 90-stopniowy łuk od godziny 12 do 9 --- */ gdk_draw_arc (obszar_rys, gc, FALSE, 0, 0, 30, 30, (90 * 64), (360 * 64));
Wyświetlanie tekstu Do wyświetlania tekstu wewnątrz obszaru rysunkowego służy funkcja gdk_draw_string. Oprócz obszaru rysunkowego i GdkGC należy przekazać do niej żądaną czcionkę; pod innymi względami jest podobna do pozostałych funkcji graficznych. /* --- wyświetlamy "Hej!" --- */ gdk_draw_string (obszar_rys, czcionka, gc, 50, 50, "Hej!");
Funkcja gdk_draw_text robi to samo, co gdk_draw_string, ale przyjmuje także dodatkowy parametr - długość łańcucha. /* --- wyświetlamy "Hej!" --- */
239 szKomunikat = "Hej!"; gdk_draw_text (obszar_rys, czcionka, gc, 50, 50, szKomunikat, strlen (szKomunikat));
Rysowanie piksmap Kiedy rysowanie linii i punktów okaże się niewystarczające, można skorzystać z funkcji gdk_draw_pixmap, aby skopiować kompletny rysunek do obszaru rysunkowego. Więcej informacji na temat tej przydatnej funkcji zawiera podrozdział „Usuwanie migotania”. /* --- Kopiujemy piksmapę 40 x 40 do obszaru rysunkowego --- */ gdk_draw_pixmap (obszar_rys, gc, piksmapa, 0, 0, 0, 0, 40, 40);
Kontrolka obszaru rysunkowego Kontrolka obszaru rysunkowego jest bardzo prosta. Większość kontrolek, które omawialiśmy do tej pory była pojemnikami albo posiadała przypisane im właściwości. Obszar rysunkowy posiada bardzo nieliczne właściwości w porównaniu z innymi kontrolkami. Ma rozmiar, który można ustawić przy pomocy funkcji gtk_drawing_area_size, i to mniej więcej wszystko. Jedynym zadaniem kontrolki jest udostępnienie obszaru, na którym mogą rysować funkcje GDK. Kontrolkę tworzymy przy pomocy funkcji gtk_drawing_area_new. /* --- Tworzymy kontrolkę --- */ obszar_rys = gtk_drawing_area_new (); /* --- ustawiamy odpowiednie rozmiary --- */ gtk_drawing_area_size (obszar_rys, 200, 200);
Zdarzenia w obszarze rysunkowym Dwoma zdarzeniami, które mają znaczenie w przypadku obszaru rysunkowego, są configure_event i expose_event. Sygnał configure_event jest wysyłany po to, aby wskazać, że obszar rysunkowy został utworzony albo zmienił się jego rozmiar. Sygnał expose_event jest wysyłany wtedy, kiedy konieczne jest przerysowanie obszaru: kiedy rysowana jest kontrolka obszaru rysunkowego lub kiedy inne okno zostanie odsunięte znad obszaru (odsłaniając go). Może zostać także wysłany w odpowiedzi na żądanie przerysowania, wygenerowane przez samą aplikację.
Część III Rysowanie, kolor i GDK
240 Prosta aplikacja zegara
Aplikacja zegara ilustruje korzystanie z obszaru rysunkowego i funkcji GDK. Konieczne jest narysowanie zegara na ekranie (łuk i kilka kresek, oznaczających godziny), oraz narysowanie wskazówek zegara i uaktualnianie ich pozycji co sekundę. Zegar korzysta z czasomierza, aby co sekundę uaktualniać wyświetlany czas i odświeżać cały obszar rysunkowy. Rysunek 10.3 przedstawia cel, do którego zmierzamy.
Rysunek 10.3. Aplikacja zegara.
Oczywiste jest, że potrzebny będzie czasomierz, który co sekundę będzie rysował na zegarze aktualny czas. Aplikacja zegara ustawia czasomierz przy pomocy funkcji gtk_timeout_add, nakazując wywoływanie funkcji Przerysuj. Funkcja Przerysuj co sekundę czyści obszar rysunkowy i przerysowuje cały zegar. Oto kod: /* * Autor: Eric Harlow * * */ #include #include #include int promien;
241 /* * NarysujKreske * * Rysuje kreskę na tarczy zegara. Kreski rysujemy przy * godzinach, aby łatwiej było odczytać czas na zegarze. * * piksmapa - obszar rysunkowy * gc - pióro * nGodzina - 1-12, przy jakiej godzinie narysować kreskę * cx - szerokość zegara * cy - wysokość zegara */ void NarysujKreske (GdkDrawable *piksmapa, GdkGC *gc, int nGodzina, int cx, int cy) { /* --- Przekształcamy godzinę na kąt w radianach --- */ double dRadiany = nGodzina * 3.14 / 6.0; /* --- Rysujemy linię od .95 * rad do 1.0 * rad --- */ gdk_draw_line (piksmapa, gc, cx+(int) ((0.95 * promien * sin (dRadiany))), cy-(int) ((0.95 * promien * cos (dRadiany))), cx+(int) ((1.0 * promien * sin (dRadiany))), cy-(int) ((1.0 * promien * cos (dRadiany)))); } /* * Przerysuj * * dane - kontrolka do przerysowania */ gint Przerysuj (gpointer dane) { GtkWidget* obszar_rys = (GtkWidget *) dane; int midx, midy; int nGodzina; float dRadiany; time_t teraz; struct tm *teraz_tm; GdkDrawable *rysunek;
Część III Rysowanie, kolor i GDK
242
/* --- Pobieramy okno z obszarem rysunkowym --- */ rysunek = obszar_rys->window; /* --- Czyścimy obszar, rysując prostokąt --- */ gdk_draw_rectangle (rysunek, obszar_rys->style->white_gc, TRUE, 0, 0, obszar_rys->allocation.width, obszar_rys->allocation.height); /* --- Określamy punkt środkowy --- */ midx = obszar_rys->allocation.width / 2; midy = obszar_rys->allocation.height / 2; /* --- Znajdujemy mniejszą wartość --- */ promien = MIN (midx, midy); /* --- Rysujemy tarczę zegara (okrąg) --- */ gdk_draw_arc (rysunek, obszar_rys->style->black_gc, 0, 0, 0, midx + midx, midy + midy, 0, 360 * 64); /* --- Rysujemy kreski przy godzinach --- */ for (nGodzina = 1; nGodzina style->black_gc, nGodzina, midx, midy); } /* --- Pobieramy aktualny czas --- */ time (&teraz); /* --- Przekształcamy czas --- */ teraz_tm = localtime (&teraz); /* * --- Rysujemy wskazówkę sekundową */ /* --- Obliczamy radiany na podstawie liczby sekund --- */ dRadiany = teraz_tm->tm_sec * 3.14 / 30.0;
243 /* --- Rysujemy wskazówkę --- */ gdk_draw_line (rysunek, obszar_rys->style->black_gc, midx, midy, midx + (0.9 * promien * sin (dRadiany)), midy - (0.9 * promien * cos (dRadiany))); /* * --- Rysujemy wskazówkę minutową */ /* --- Obliczamy radiany na podstawie liczby minut i sekund --- */ dRadiany = (teraz_tm->tm_min * 3.14 / 30.0) + (3.14 * teraz_tm->tm_sec / 1800.0); /* --- Rysujemy wskazówkę --- */ gdk_draw_line (rysunek, obszar_rys->style->black_gc, midx, midy, midx+(int) (0.7 * promien * sin (dRadiany)), midy-(int) (0.7 * promien * cos (dRadiany))); /* * --- Rysujemy wskazówkę godzinową */ /* --- Przeliczamy godzinę na radiany --- */ dRadiany = (teraz_tm->tm_hour % 12) * 3.14 / 6.0 + (3.14 * teraz_tm->tm_min / 360.0); /* --- Rysujemy wskazówkę --- */ gdk_draw_line (rysunek, obszar_rys->style->black_gc, midx, midy, midx + (int) (promien * 0.5 * sin (dRadiany)), midy - (int) (promien * 0.5 * cos (dRadiany))); return (TRUE); } /* * zamknij * * opuszczamy pętlę zdarzeń gtk */ void zamknij ()
Część III Rysowanie, kolor i GDK
244 { gtk_exit (0); } /* * main * * Program zaczyna się tutaj */ int main (int argc, char *argv[]) { GtkWidget *okno; GtkWidget *obszar_rys; GtkWidget *ypole; /* --- Inicjacja GTK --- */ gtk_init (&argc, &argv);
/* --- Tworzymy okno najwyższego poziomu --- */ okno = gtk_window_new (GTK_WINDOW_TOPLEVEL); /* --- Tworzymy pole pakujące --- */ ypole = gtk_hbox_new (FALSE, 0); /* --- Dodajemy pole pakujące do okna --- */ gtk_container_add (GTK_CONTAINER (okno), ypole); /* --- Uwidaczniamy pole pakujące --- */ gtk_widget_show (ypole); /* --- Czekamy na sygnał "destroy" --- */ gtk_signal_connect (GTK_OBJECT (okno), "destroy", GTK_SIGNAL_FUNC (zamknij), NULL); /* --- Tworzymy obszar rysunkowy --- */ obszar_rys = gtk_drawing_area_new (); /* --- Ustawiamy jego rozmiary --- */ gtk_drawing_area_size (GTK_DRAWING_AREA (obszar_rys), 200, 200); /* --- Dodajemy obszar rysunkowy do pola pakującego --- */ gtk_box_pack_start (GTK_BOX (ypole), obszar_rys, TRUE, TRUE, 0); /* --- Uwidaczniamy obszar rysunkowy --- */
245 gtk_widget_show (obszar_rys); /* --- Uwidaczniamy okno --- */ gtk_widget_show (okno); /* --- Przerysowujemy obszar co 1000 ms (co sekundę) --- */ gtk_timeout_add (1000, Przerysuj, (gpointer) obszar_rys); /* --- Uruchamiamy pętlę obsługi zdarzeń gtk --- */ gtk_main (); return 0; }
Z jakimi problemami będziemy musieli się uporać? Po pierwsze, zegar będzie migotał na wolnych komputerach, a także na szybszych, choć w nieco mniejszym stopniu. Migotanie jest spowodowane przez procedurę przerysowującą. Ekran komputera jest nieustannie odświeżany (wiele razy na sekundę). Jeśli sprzęt odświeży wyświetlany na monitorze obraz zaraz po wyczyszczeniu obszaru rysunkowego przez kod, wówczas przez krótki moment na ekranie pojawi się czysty (albo tylko częściowo przerysowany) obszar rysunkowy. Oczywiście, przy następnym sprzętowym odświeżeniu ekranu rysowanie kontrolki prawdopodobnie dobiegnie końca, ale będzie już za późno - ludzkie oko nie dostrzeże co prawda pustego zegara, ale zauważy migotanie. Zegar ma szansę migotać tylko raz na sekundę, kiedy jest przerysowywany. Im bardziej skomplikowany jest rysunek, tym większe prawdopodobieństwo, że nie zostanie on dokończony pomiędzy kolejnymi odświeżeniami obrazu, a użytkownik będzie widział migotanie. Jest to niepożądany efekt, wiec będziemy musieli się go pozbyć. Drugi problem stanie się widoczny, kiedy kilkakrotnie spróbujemy przykryć zegar jakimś oknem, a następnie odsunąć je znad zegara. Za którymś razem zauważymy długie opóźnienie pomiędzy odsłonięciem zegara a uaktualnieniem wyświetlanego obrazu. Opóźnienie to nigdy nie przekracza sekundy, a spowodowane jest tym, że aplikacja nie oczekuje na sygnał expose_event, który informuje, że okno zostało odsłonięte i wymaga przerysowania. Poprawna aplikacja GDK powinna zawierać funkcję zwrotną dla sygnału expose_event i uaktualniać zegar natychmiast, nie czekając na następny sygnał od czasomierza.
246
Część III Rysowanie, kolor i GDK
Usuwanie migotania Podstawowy problem w aplikacji polega na tym, że zegar migocze co sekundę w trakcie przerysowywania. Najwygodniejszą metodą usunięcia tego efektu jest użycie techniki znanej jako podwójne buforowanie (double buffering). W metodzie tej korzystamy z bufora, używanego jako płótno dla wszelkich operacji graficznych. Obraz na ekranie nie jest modyfikowany; rysowanie przeprowadza się w buforze. W naszym przykładzie możemy utworzyć bufor o takich samych rozmiarach jak okno, i rysować zegar w buforze, zamiast bezpośrednio w oknie. Kiedy zakończymy rysowanie zegara w buforze, skopiujemy go do okna. Ponieważ przenoszenie bufora do okna nie wymaga czyszczenia ekranu, migotanie nie będzie widoczne. Pierwszymi krokami na drodze do eliminacji migotania podczas animacji zegara jest sprawdzanie sygnałów expose_event i configure_event. Sygnał configure_event jest wysyłany podczas tworzenia obszaru rysunkowego albo zmiany jego rozmiarów. W tym momencie możemy utworzyć drugoplanowy bufor, o rozmiarze równym obszarowi rysunkowemu. Jeśli rozmiary okna ulegną zmianie, funkcja zwrotna dla sygnału configure_ event ponownie utworzy bufor o właściwym rozmiarze.
configure_event Funkcja zwrotna dla sygnału configure_event tworzy drugoplanowy bufor, w którym będziemy rysować zegar, nie naruszając obrazu wyświetlanego na ekranie. Bufor ten będzie piksmapą; możemy utworzyć piksmapę o rozmiarach równych obszarowi rysunkowemu przy pomocy funkcji gdk_pixmap_new. Jeśli zmieni się rozmiar kontrolki obszaru rysunkowego, należy ponownie przydzielić miejsce na piksmapę. Funkcja zwrotna configure_event jest wywoływana wtedy, kiedy rozmiar kontrolki ulega zmianie, co umożliwia nam utworzenie nowej, drugoplanowej piksmapy o odpowiednio uaktualnionych rozmiarach. Ponieważ poprzednia piksmapa jest już niepotrzebna, należy wywołać funkcję gdk_pixmap_unref, aby poinformować GDK, że nie odwołujemy się już do piksmapy, więc można ją zwolnić - o ile nie odwołuje się do niej ktoś inny. Funkcja zwrotna dla sygnału expose_event wygląda więc następująco: static gint configure_event (GtkWidget *kontrolka, GdkEventConfigure *zdarzenie) { /* --- zwalniamy bufor, jeśli już istnieje --- */ if (piksmapa) { gdk_pixmap_unref (piksmapa);
247 } /* --- tworzymy piksmapę o nowych rozmiarach --- */ piksmapa = gdk_pixmap_new (kontrolka->window, kontrolka->allocation.width, kontrolka->allocation.height, -1); return TRUE; }
expose_event Sygnał expose_event jest bardzo prosty w obsłudze. Ponieważ rysujemy zegar w drugoplanowej piksmapie, jedyną czynnością po wystąpieniu expose_event jest skopiowanie drugoplanowego rysunku do naszego okna przy pomocy funkcji gdk_draw_pixmap. Obsługa sygnału expose_ event usuwa drugi problem, związany z zegarem: opóźnienie w odświeżaniu obrazu po odsunięciu innego okna znad zegara. Teraz, kiedy okno zostanie odsunięte znad zegara, aplikacja obsłuży sygnał expose_event, kopiując ostatni obraz zegara (piksmapę) do swojego okna. Sygnał expose_event jest wysyłany wraz z dodatkowym parametrem, zawierającym dane o zdarzeniu. Pole area tego parametru określa obszar okna, który został odsłonięty i wymaga przerysowania. Przydaje się to w sytuacjach, kiedy na ekranie jest wiele nakładających się na siebie okien i jedno z nich zostanie przesunięte; wówczas najszybszą metodą jest przerysowanie tylko tego obszaru, który był ukryty pod innym oknem. W naszym przykładowym programie musielibyśmy wymusić przerysowanie całego zegara. Funkcja zwrotna dla sygnału expose_event wygląda więc następująco: gint expose_event (GtkWidget *kontrolka, GdkEventExpose *zdarzenie) { /* --- Kopiujemy piksmapę do okna --- */ gdk_draw_pixmap (kontrolka->window, kontrolka->style->fg_gc[GTK_WIDGET_STATE (kontrolka)], piksmapa, zdarzenie->area.x, zdarzenie->area.y, zdarzenie->area.x, zdarzenie->area.y, zdarzenie->area.width, zdarzenie->area.height); return FALSE; }
248
Część III Rysowanie, kolor i GDK
Prawdopodobnie kilka rzeczy wymaga jeszcze wyjaśnienia. Jeśli rysujemy na drugoplanowej piksmapie, jak zamierzamy uaktualnić obraz na ekranie? Po narysowaniu zegara na piksmapie musimy spowodować, aby funkcja zwrotna expose_event skopiowała piksmapę do okna. Definiujemy w tym celu obszar, który ma zostać odsłonięty (cały obszar rysunkowy) i wywołujemy funkcję gtk_widget_draw, przekazując jej kontrolkę i obszar, który wymaga uaktualnienia. /* --- definiujemy obszar, wymagający przerysowania --- */ uakt_prostokat.x = 0; uakt_prostokat.y = 0; uakt_prostokat.width = obszar_rys->allocation.width; uakt_prostokat.height = obszar_rys->allocation.height; /* --- rysujemy obszar (powoduje wywołanie expose_event) --- */ gtk_widget_draw (obszar_rys, &uakt_prostokat);
Poniżej znajduje się nowa wersja zegara, z zaimplementowanym podwójnym buforowaniem. Większość kodu przypomina pierwotną wersję. Zmiany są nieznaczne, ale efekty uderzające. /* * Autor: Eric Harlow * * */ #include #include #include /* --- piksmapa - bufor obszaru rysunkowego --- */ static GdkPixmap *piksmapa = NULL; int promien; /* * NarysujKreske * * Rysuje kreskę na tarczy zegara. Kreski rysujemy przy * godzinach, aby łatwiej było odczytać czas na zegarze. * * piksmapa - obszar rysunkowy * gc - pióro
249 * nGodzina - 1-12, przy jakiej godzinie narysować kreskę * cx - szerokość zegara * cy - wysokość zegara */ void NarysujKreske (GdkDrawable *piksmapa, GdkGC *gc, int nGodzina, int cx, int cy) { /* --- Przekształcamy godzinę na kąt w radianach --- */ double dRadiany = nGodzina * 3.14 / 6.0; /* --- Rysujemy linię od .95 * rad do 1.0 * rad --- */ gdk_draw_line (piksmapa, gc, cx+(int) ((0.95 * promien * sin (dRadiany))), cy-(int) ((0.95 * promien * cos (dRadiany))), cx+(int) ((1.0 * promien * sin (dRadiany))), cy-(int) ((1.0 * promien * cos (dRadiany)))); } /* * Przerysuj * * dane - kontrolka do przerysowania */ gint Przerysuj (gpointer dane) { GtkWidget* obszar_rys = (GtkWidget *) dane; GdkRectangle uakt_prostokat; char bufor[88]; int midx, midy; int nGodzina; float dRadiany; time_t teraz; struct tm *teraz_tm; /* --- Czyścimy piksmapę (rysując drugoplanowy prostokąt) --- */ gdk_draw_rectangle (piksmapa, obszar_rys->style->white_gc, TRUE, 0, 0, obszar_rys->allocation.width, obszar_rys->allocation.height);
Część III Rysowanie, kolor i GDK
250
/* --- Określamy punkt środkowy zegara --- */ midx = obszar_rys->allocation.width / 2; midy = obszar_rys->allocation.height / 2; /* --- obliczmy promień --- */ promien = MIN (midx, midy); /* --- Rysujemy okrąg --- */ gdk_draw_arc (piksmapa, obszar_rys->style->black_gc, 0, 0, 0, midx + midx, midy + midy, 0, 360 * 64); /* --- Rysujemy kreski przy godzinach --- */ for (nGodzina = 1; nGodzina style->black_gc, nGodzina, midx, midy); } /* --- Pobieramy czas --- */ time (&teraz); teraz_tm = localtime (&teraz); /* * --- Rysujemy wskazówkę sekundową */ /* --- Obliczamy radiany na podstawie liczby sekund --- */ dRadiany = teraz_tm->tm_sec * 3.14 / 30.0; /* --- Rysujemy wskazówkę --- */ gdk_draw_line (piksmapa, obszar_rys->style->black_gc, midx, midy, midx + (0.9 * promien * sin (dRadiany)), midy - (0.9 * promien * cos (dRadiany))); /* * --- Rysujemy wskazówkę minutową */ /* --- Obliczamy radiany na podstawie liczby minut i sekund --- */ dRadiany = (teraz_tm->tm_min * 3.14 / 30.0) + (3.14 * teraz_tm->tm_sec / 1800.0);
251 /* --- Rysujemy wskazówkę --- */ gdk_draw_line (piksmapa, obszar_rys->style->black_gc, midx, midy, midx+(int) (0.7 * promien * sin (dRadiany)), midy-(int) (0.7 * promien * cos (dRadiany))); /* * --- Rysujemy wskazówkę godzinową */ /* --- Przeliczamy godzinę na radiany --- */ dRadiany = (teraz_tm->tm_hour % 12) * 3.14 / 6.0 + (3.14 * teraz_tm->tm_min / 360.0); /* --- Rysujemy wskazówkę --- */ gdk_draw_line (piksmapa, obszar_rys->style->black_gc, midx, midy, midx + (int) (promien * 0.5 * sin (dRadiany)), midy - (int) (promien * 0.5 * cos (dRadiany))); /* --- Ustawiamy prostokąt * --- Do okna będzie kopiowana cała piksmapa, więc * --- prostokąt ma rozmiary równe obszarowi okna */ uakt_prostokat.x = 0; uakt_prostokat.y = 0; uakt_prostokat.width = obszar_rys->allocation.width; uakt_prostokat.height = obszar_rys->allocation.height; /* --- rysujemy obszar (powoduje wywołanie expose_event) --- */ gtk_widget_draw (obszar_rys, &uakt_prostokat); } /* * configure_event * * tworzy nową, drugoplanową piksmapę o odpowiednich rozmiarach. * Jest wywoływana przy każdej zmianie rozmiarów okna. Musimy * zwolnić przydzielone zasoby */ static gint configure_event (GtkWidget *kontrolka, GdkEventConfigure *zdarzenie); {
Część III Rysowanie, kolor i GDK
252
/* --- zwalniamy bufor, jeśli go utworzyliśmy --- */ if (piksmapa) { gdk_pixmap_unref (piksmapa); } /* --- tworzymy piksmapę o nowych rozmiarach --- */ piksmapa = gdk_pixmap_new (kontrolka->window, kontrolka->allocation.width, kontrolka->allocation.height, -1); return TRUE; } /* * expose_event * * Funkcja ta wywoływana jest po odsłonięciu okna albo po wywołaniu * funkcji gdk_widget_draw. Kopiuje drugoplanową piksmapę do okna. */ gint expose_event (GtkWidget *kontrolka, GdkEventExpose *zdarzenie); { /* --- Kopiujemy piksmapę do okna --- */ gdk_draw_pixmap (kontrolka->window, kontrolka->style->fg_gc[GTK_WIDGET_STATE (kontrolka)], piksmapa, zdarzenie->area.x, zdarzenie->area.y, zdarzenie->area.x, zdarzenie->area.y, zdarzenie->area.width, zdarzenie->area.height); return FALSE; } /* * zamknij * * Wychodzimy z aplikacji */ void zamknij () {
253 gtk_exit (0); } /* * main * * Program zaczyna się tutaj */ int main (int argc, char *argv[]) { GtkWidget *okno; GtkWidget *obszar_rys; GtkWidget *ypole; gtk_init (&argc, &argv); okno = gtk_window_new (GTK_WINDOW_TOPLEVEL); ypole = gtk_hbox_new (FALSE, 0); gtk_container_add (GTK_CONTAINER (okno), ypole); gtk_widget_show (ypole); gtk_signal_connect (GTK_OBJECT (okno), "destroy", GTK_SIGNAL_FUNC (zamknij), NULL); /* --- Tworzymy obszar rysunkowy --- */ obszar_rys = gtk_drawing_area_new (); gtk_drawing_area_size (GTK_DRAWING_AREA (obszar_rys), 200, 200); gtk_box_pack_start (GTK_BOX (ypole), obszar_rys, TRUE, TRUE, 0); gtk_widget_show (obszar_rys); /* --- Sygnały używane do obsługi drugoplanowej piksmapy --- */ gtk_signal_connect (GTK_OBJECT (obszar_rys), "expose_event", (GtkSignalFunc) expose_event, NULL); gtk_signal_connect (GTK_OBJECT (obszar_rys), "configure_event", (GtkSignalFunc) configure_event, NULL); /* --- Uwidaczniamy okno --- */ gtk_widget_show (okno); /* --- Przerysowujemy obszar co sekundę --- */ gtk_timeout_add (1000, Przerysuj, (gpointer) obszar_rys);
Część III Rysowanie, kolor i GDK
254 /* --- Wywołujemy główną pętlę gtk --- */ gtk_main (); return 0; }
Monitor systemowy Wykorzystamy ponownie niektóre z poprzednio napisanych funkcji, aby stworzyć niewielki program, który monitoruje komputer i wyświetla wykres obrazujący stan procesora oraz połączeń ethernetowych (patrz rysunek 10.4). Jedynym problemem jest pozyskanie informacji o systemie.
Rysunek 10.4. Monitor systemowy.
Wykorzystanie systemu plików /proc Większość danych o stanie systemu Linux można odczytać z systemu plików /proc. „Pliki” w katalogu /proc można otwierać i czytać jak zwykłe pliki. Znajduje się w nich wiele informacji o komputerze - na przykład plik /proc/loadavg zawiera dane o obciążeniu procesora. Aby dowiedzieć się, do czego służą pozostałe pliki, można wpisać polecenie man proc, które wyświetli skrótową informację o wszystkich plikach w systemie /proc. Jeśli wyświetlimy plik /proc/loadavg poleceniem cat, zobaczymy rezultaty podobne do zamieszczonych niżej: [bystry@umysl /proc]$ cat loadavg 0.00 0.00 0.00 2/47 649
255 Pierwsze trzy liczby oznaczają średnie obciążenie procesora w ostatniej minucie, ostatnich 5 minutach i ostatnich 15 minutach. Jeśli wydamy polecenie w z linii poleceń Linuksa, otrzymamy mniej więcej następujące rezultaty: 4:15poleceniem up 3:44, 4 users, load average: 0.00, 0.00, 0.00
W powyższej linii średnie obciążenie jest określone przez ostatnie trzy liczby (Hmmm... komputer nie jest zbyt zapracowany). Innym przykładowym plikiem w systemie /proc jest /proc/net/dev, zawierający informacje o pakietach wysyłanych i odbieranych przez wszystkie urządzenia w systemie. W komputerze autora rezultaty przeglądania pliku wyglądają następująco: Inter-| Receive face |packets errs drop fifo frame | packets errs drop fifo colls carrier lo: 103 0 0 0 0 103 0 0 0 0 0 eth0: 9600 0 0 0 0 6487 0 0 0 0 0
Oczywiście, przy każdym przeglądaniu pliku liczba wysłanych i odebranych pakietów będzie inna. Jeśli wpiszemy to samo polecenie trochę później, otrzymamy: Inter-| Receive face |packets errs drop fifo frame | packets errs drop fifo colls carrier lo: 103 0 0 0 0 103 0 0 0 0 0 eth0: 9600 0 0 0 0 6487 0 0 0 0 0
Urządzenie, które będziemy monitorować, to eth0:. Jest to karta Ethernet w komputerze; chcemy wiedzieć, jaki w danym momencie panuje na niej ruch. Możemy obliczyć aktualne natężenie ruchu, porównując bieżące dane o pakietach z danymi pochodzącymi z poprzedniego odczytu pliku.
Opis Aplikacja korzysta z czasomierza, aby uaktualniać ekran co kilka sekund. Odczytuje statystyki z systemu plików /proc i wyświetla informacje w postaci wykresu, na którym każdy element jest zaznaczony odmien-
256
Część III Rysowanie, kolor i GDK
nym kolorem. Wykres można konfigurować przy pomocy przycisków na pasku narzędziowym. Autor przeprowadził kilka testów i zauważył, że wykres obciążenia sieci miał tendencje do znacznych fluktuacji, co utrudniało zmierzenie rzeczywistej przepustowości. Pomocne okazało się dodanie kolejnego przycisku, który wyświetla średnią przepustowość. Średnia jest obliczana na podstawie ostatnich N próbek i daje bardziej zrównoważony wykres. Program monitora systemowego jest podzielony na kilka modułów. W każdym z nich umieszczono specyficzny kod, który wykonuje swoją część pracy na rzecz aplikacji. Opis zaczniemy od pliku interfejs.c.
interfejs.c interfejs.c tworzy okno aplikacji, pasek narzędziowy (z piksmapami) oraz niewielkie menu. Posiada także procedury używane przez inne funkcje, które sprawdzają stan interfejsu użytkownika. Można na przykład wywołać funkcję WcisnietoPrzyciskCPU15, aby sprawdzić, czy użytkownik wcisnął przycisk paska narzędziowego, który wyświetla obciążenie procesora w ostatnich 15 minutach. Rozwiązanie takie jest lepsze, niż używanie globalnej zmiennej pasek_cpu15 na zewnątrz pliku (rodzaj kapsułkowania danych w języku C). Zwróćmy uwagę, że przyciski paska narzędziowego mają różne kolory. Kolory przycisków odpowiadają kolorom na wykresie, co ułatwia użytkownikowi włączanie i wyłączanie różnych elementów wykresu. Tego typu wizualne wskazówki pomagają powiązać elementy wykresu z przyciskami paska narzędziowego. /* * Interfejs przykładowej aplikacji GUI. * * Autor: Eric Harlow * Plik: frontend.c * */ #include #include #include #include /* * --- Prototypy funkcji */ GtkWidget *UtworzObszarRysunkowy ();
257 static void UtworzGlowneOkno (); void UtworzPasek (GtkWidget *glowne_ypole); void UstawPrzyciskPaska (char *szPrzycisk, int nStan); void ZaznaczMenu (GtkWidget *kontrolka, gpointer dane); void OdznaczMenu (GtkWidget *kontrolka, gpointer dane); void UstawPrzyciskMenu (char *szPrzycisk, int nStan) ; GtkWidget *UtworzKontrolkeZXpm (GtkWidget *okno, gchar **dane_xpm); GtkWidget *UtworzElementMenu (GtkWidget *menu, char *szNazwa, char *szSkrot, char *szPodpowiedz, GtkSignalFunc funkcja, gpointer dane); GtkWidget *UtworzZaznaczalnyElement (GtkWidget *menu, char *szNazwa, GtkSignalFunc funkcja, gpointer dane); GtkWidget *UtworzPodmenu (GtkWidget *pasek, char *szNazwa); GtkWidget *UtworzPodmenuPaska (GtkWidget *menu, char *szNazwa); void Przerysuj (); /* * --- Zmienne globalne */ GtkWidget GtkTooltips GtkAccelGroup GtkWidget
*glowne_okno; *podpowiedzi; *grupa_skrotow; *pasek_narz;
static GtkWidget static GtkWidget static GtkWidget static GtkWidget static GtkWidget
*pasek_cpu1; *pasek_cpu5; *pasek_cpu15; *pasek_siec; *pasek_siec_srednia;
/* * --- Bitmapa dla przycisku "przepustowość sieci" */ static const gchar *xpm_siec[] = { "16 16 4 1", " c None",
258
Część III Rysowanie, kolor i GDK
"B c #888888", " ", " ", " BBB ", " BBBBBBB ", " BBB B ", " B ", " B ", " B ", " B ", " B ", " B ", " B BBB ", " BBBBBB ", " BBB ", " ", " ", }; /* * --- Bitmap dla przycisku "średnia przepustowość sieci" */ static const gchar *xpm_siec_srednia[] = { "16 16 4 1", " c None", "B c #000000", " ", " ", " BBB ", " BBBBBBB ", " BBB B ", " B ", " B ", " B ", " B ", " B ", " B ", " B BBB ", " BBBBBB ", " BBB ",
259 " " };
", ",
/* * --- Bitmapa dla przycisku "obciążenie cpu w ostatniej minucie" */ static const char *xpm_cpu1[] = { "16 16 2 1", " c None", "B c #FF0000", " ", " B B B B B B ", " BBBBBBBBBBBBB ", " BB B ", " B B BB", " BB BB B ", " B B BB", " BB B B ", " B B BB", " BB B B ", " B B BB", " BB BBB B ", " B BB", " BBBBBBBBBBBBB ", " B B B B B B ", " ", }; /* * --- Bitmapa dla przycisku "obciążenie cpu w ostatnich 5 minutach" */ static const char *xpm_cpu5[] = { "16 16 2 1", " c None", "B c #00FF00", " ", " B B B B B B ", " BBBBBBBBBBBBB ", " BB B ", " B BBBBB BB",
260
Część III Rysowanie, kolor i GDK
" BB B B ", " B B BB", " BB BBBB B ", " B B BB", " BB B B ", " B B B BB", " BB BBB B ", " B BB", " BBBBBBBBBBBBB ", " B B B B B B ", " ", }; /* * --- Bitmapa dla przycisku "obciążenie cpu w ostatnich 15 minutach” */ static const char *xpm_cpu15[] = { "16 16 2 1", " c None", "B c #0000FF", " ", " B B B B B B ", " BBBBBBBBBBBBB ", " BB B ", " B B BBBBB BB", " BB BB B B ", " B B B BB", " BB B BBBB B ", " B B B BB", " BB B B B ", " B B B B BB", " BB BBB BBB B ", " B BB", " BBBBBBBBBBBBB ", " B B B B B B ", " ", }; /* * PrzerysujWykres *
261 * Wywoływana wtedy, kiedy użytkownik przełącza przyciski na * pasku narzędziowym, aby zmienić informacje wyświetlane na wykresie. * Wywołuje funkcję "Przerysuj", aby wymusić przerysowanie * wykresu. */ void PrzerysujWykres (GtkWidget *kontrolka, gpointer dane) { Przerysuj (); } /* * WcisnietoPrzyciskSiec * * Czy przycisk "sieć" na pasku narzędziowym jest wciśnięty? */ int WcisnietoPrzyciskSiec () { return (GTK_TOGGLE_BUTTON (pasek_siec)->active); } /* * WcisnietoPrzyciskSiecSrednia * * Czy przycisk "średnia sieci" na pasku narzędziowym jest wciśnięty? */ int WcisnietoPrzyciskSiecSrednia () { return (GTK_TOGGLE_BUTTON (pasek_siec_srednia)->active); } /* * WcisnietoPrzyciskCPU1 * * Czy przycisk "CPU-1" na pasku narzędziowym jest wciśnięty? */ int WcisnietoPrzyciskCPU1 () { return (GTK_TOGGLE_BUTTON (pasek_cpu1)->active); } /* * WcisnietoPrzyciskCPU5
Część III Rysowanie, kolor i GDK
262
* * Czy przycisk "CPU-5" na pasku narzędziowym jest wciśnięty? */ int WcisnietoPrzyciskCPU5 () { return (GTK_TOGGLE_BUTTON (pasek_cpu5)->active); } /* * WcisnietoPrzyciskCPU15 * * Czy przycisk "CPU-15" na pasku narzędziowym jest wciśnięty? */ int WcisnietoPrzyciskCPU15 () { return (GTK_TOGGLE_BUTTON (pasek_cpu15)->active); } /* * KoniecProgramu * * Wyjście z programu */ void KoniecProgramu () { gtk_main_quit (); } /* * UtworzGlowneOkno * * Tworzy główne okno i związane z nim menu/pasek narzędziowy. */ static void UtworzGlowneOkno () { GtkWidget *kontrolka; GtkWidget *glowne_ypole; GtkWidget *pasekmenu; GtkWidget *menu; GtkWidget *elmenu; /* --- Tworzymy główne okno --- */
263 glowne_okno = gtk_window_new(GTK_WINDOW_TOPLEVEL); /* --- Nie pozwalamy na zmianę rozmiarów okna --- */ gtk_window_set_policy (GTK_WINDOW (glowne_okno), FALSE, FALSE, TRUE); /* --- Tytu³ --- */ gtk_window_set_title (GTK_WINDOW (glowne_okno), "Monitor systemowy"); gtk_container_border_width (GTK_CONTAINER (glowne_okno), 0); /* --- Tworzymy tablicę skrótów klawiszowych --- */ grupa_skrotow = gtk_accel_group_new (); gtk_accel_group_attach (grupa_skrotow, GTK_OBJECT (glowne_okno)); /* --- Główne okno musi sprawdzać sygnał "destroy" --- */ gtk_signal_connect (GTK_OBJECT (glowne_okno), "destroy", GTK_SIGNAL_FUNC(KoniecProgramu), NULL); /* --- Tworzymy pionowe pole pakujące dla menu/paska narz. --- */ glowne_ypole = gtk_vbox_new (FALSE, 0); /* --- Wyświetlamy pionowe pole pakujące --- */ gtk_container_add (GTK_CONTAINER (glowne_okno), glowne_ypole); gtk_widget_show (glowne_ypole); gtk_widget_show (glowne_okno); /* --- Pasek menu --- */ pasekmenu = gtk_menu_bar_new (); gtk_box_pack_start (GTK_BOX (glowne_ypole), pasekmenu, FALSE, TRUE, 0); gtk_widget_show (pasekmenu); /* -------------------- Menu Plik ------------------- */ menu = UtworzPodmenuPaska (pasekmenu, "Plik"); elmenu = UtworzElementMenu (menu, "Zakończ", "", "Czy istnieje bardziej wymowna opcja?", GTK_SIGNAL_FUNC (KoniecProgramu), "zakończ"); /* --- Tworzymy pasek narzędziowy --- */
Część III Rysowanie, kolor i GDK
264 UtworzPasek (glowne_ypole);
/* --- Tworzymy obszar do wyświetlania informacji --- */ kontrolka = UtworzObszarRysunkowy (); gtk_box_pack_start (GTK_BOX (glowne_ypole), kontrolka, TRUE, TRUE, 0); } /* * UtworzPasek * * Tworzy pasek narzędziowy, którego przyciski będą włączały i * wyłączały elementy wykresu w obszarze rysunkowym. */ void UtworzPasek (GtkWidget *glowne_ypole) { /* --- Tworzymy pasek narzędziowy i dodajemy go do okna --- */ pasek_narz = gtk_toolbar_new (GTK_ORIENTATION_HORIZONTAL, GTK_TOOLBAR_ICONS); gtk_box_pack_start (GTK_BOX (glowne_ypole), pasek_narz, FALSE, TRUE, 0); gtk_widget_show (pasek_narz); /* --- Informacja o przepływie pakietów --- */ pasek_siec = gtk_toolbar_append_element (GTK_TOOLBAR (pasek_narz), GTK_TOOLBAR_CHILD_TOGGLEBUTTON, NULL, "Sieć", "Sieć", "Sieć", UtworzKontrolkeZXpm (glowne_ypole, (gchar **) xpm_siec), (GtkSignalFunc) PrzerysujWykres, NULL); /* --- Informacja o średnim przepływie pakietów --- */ pasek_siec_srednia = gtk_toolbar_append_element ( GTK_TOOLBAR (pasek_narz), GTK_TOOLBAR_CHILD_TOGGLEBUTTON, NULL, "Sieć (średnia)", " Sieć (średnia)", " Sieć (średnia)", UtworzKontrolkeZXpm (glowne_ypole,(gchar **) xpm_siec_srednia), (GtkSignalFunc) PrzerysujWykres, NULL);
265 /* --- Niewielki odstęp --- */ gtk_toolbar_append_space (GTK_TOOLBAR (pasek_narz)); /* --- Informacja o wykorzystaniu CPU w ostatniej minucie --- */ pasek_cpu1 = gtk_toolbar_append_element (GTK_TOOLBAR (pasek_narz), GTK_TOOLBAR_CHILD_TOGGLEBUTTON, NULL, "CPU 1", "CPU 1", "CPU 1", UtworzKontrolkeZXpm (glowne_ypole, (gchar **) xpm_cpu1), (GtkSignalFunc) PrzerysujWykres, NULL); /* --- Informacja o wykorzystaniu CPU w ostatnich 5 minutach -- */ pasek_cpu5 = gtk_toolbar_append_element (GTK_TOOLBAR (pasek_narz), GTK_TOOLBAR_CHILD_TOGGLEBUTTON, NULL, "CPU 5", "CPU 5", "CPU 5", UtworzKontrolkeZXpm (glowne_ypole, (gchar **) xpm_cpu5), (GtkSignalFunc) PrzerysujWykres, NULL); /* -- Informacja o wykorzystaniu CPU w ostatnich 15 minutach -- */ pasek_cpu15 = gtk_toolbar_append_element ( GTK_TOOLBAR (pasek_narz), GTK_TOOLBAR_CHILD_TOGGLEBUTTON, NULL, "CPU 15", "CPU 15", "CPU 15", UtworzKontrolkeZXpm (glowne_ypole, (gchar **) xpm_cpu15), (GtkSignalFunc) PrzerysujWykres, NULL); } /* * main * * --- Program zaczyna się tutaj */ int main(int argc, char *argv[]) { gtk_init (&argc, &argv);
Część III Rysowanie, kolor i GDK
266 podpowiedzi = gtk_tooltips_new(); UtworzGlowneOkno (); gtk_main(); return 0; }
urzadzenia.c W pliku urzadzenia.c znajdują się procedury, które przechowują urządzenia oraz dane liczbowe, na podstawie których tworzony jest wykres. Urządzenia są przechowywane na liście GSList (patrz rozdział 2, „GLIB”). Procedury te umożliwiają przechowywanie informacji o urządzeniu w postaci nazwy urządzenia i zbioru wartości, zamiast wykorzystywania odrębnej struktury danych dla każdego urządzenia. Typ typUrzadzenie, wykorzystywany w urzadzenie.c, jest zdefiniowany w urzadzenie.h. /* * urzadzenie.h * Definuje typUrzadzenie */ #define PRZYROST 1 #define RZECZYWISTA 2 #define MAKS_WARTOSCI 200 typedef long typStaraWartosc; typedef struct { char *sNazwa; int nTyp;
/* --- Nazwa urządzenia --- */ /* --- Typ wartości: PRZYROST albo RZECZYWISTA --- */ typStaraWartosc nOstatnia; /* --- Ostatnia wartość (dla PRZYROSTU) --- */ typStaraWartosc nMaks; /* --- Wartość maksymalna --- */ typStaraWartosc nWartosci [MAKS_WARTOSCI]; /* --- Zapamiętane wartości --- */ } typUrzadzenie; /* * Plik: urzadzenie.c
267 * Autor: Eric Harlow * */ #include #include #include #include "urzadzenie.h" static GSList *lista_urz = NULL; /* * SzukajUrzadzenia * * Szuka urządzenia na liście */ typUrzadzenie *SzukajUrzadzenia (char *sNazwa) { GSList *wezel; typUrzadzenie *urz; /* --- Przechodzimy przez listę urządzeń. --- */ for (wezel = lista_urz; wezel; wezel = wezel->next) { /* --- Pobieramy dane --- */ urz = (typUrzadzenie *) wezel->data; /* --- Czy właśnie tego szukamy? --- */ if (!strcmp (urz->sNazwa, sNazwa)) { return (urz); } } return (NULL); } /* * DodajUrzadzenie * * Dodaje nowe urządzenie do listy urządzeń i zwraca * je, aby można je było zainicjować */ typUrzadzenie *DodajUrzadzenie () {
Część III Rysowanie, kolor i GDK
268 typUrzadzenie *urz;
/* --- tworzymy nowe urządzenie --- */ urz = (typUrzadzenie *) g_malloc (sizeof (typUrzadzenie)); /* --- Dodajemy je do listy --- */ lista_urz = g_slist_append (lista_urz, urz); /* --- Zwracamy nowy element listy --- */ return (urz); } /* * UaktualnijUrzadzenie * * Uaktualnia informacje o urządzeniu. Jeśli urządzenie obecnie * nie istnieje, zostanie dodane i zainicjowane. * * sNazwaUrz - nazwa urządzenia. "eth0:", "cpu1", itd. * nWartosc - wartość w tym momencie. * nTyp - PRZYROST/RZECZYWISTA. * RZECZYWISTA oznacza wartość "tak, jak jest" * PRZYROST oznacza, że wartość odnosi się do poprzedniej * wartości i należy obliczyć rzeczywistą wartość, * mając to na względzie * nMaks - Jest to maksymalna wyświetlana wartość */ void UaktualnijUrzadzenie (char *sNazwaUrz, long nWartosc, int nTyp, long nMaks) { typUrzadzenie *urz; int i; /* --- Nie podano nazwy? Wracamy... --- */ if (sNazwaUrz == NULL) return; /* --- Szukamy urządzenia według nazwy --- */ urz = SzukajUrzadzenia (sNazwaUrz); /* --- Czy mamy już dane urządzenia? --- */ if (urz) { /* --- Po prostu dodajemy wartość --- */
269 NowaWartosc (urz, nWartosc, FALSE); } else { /* --- Nie mamy urządzenia! --- */ /* --- Tworzymy nowe urządzenie --- */ urz = DodajUrzadzenie (); /* --- Inicjujemy wartości --- */ urz->sNazwa = strdup (sNazwaUrz); urz->nTyp = nTyp; urz->nMaks = nMaks; /* --- Czyścimy wartości --- */ for (i = 0; i < MAKS_WARTOSCI; i++) { urz->nWartosci[i] = (typStaraWartosc) 0; } /* --- Uaktualniamy bieżącymi danymi --- */ NowaWartosc (urz, nWartosc, TRUE); } } /* * UaktualnijIstniejaceUrzadzenie * * Jeśli wiemy, że urządzenie istnieje, korzystamy z tej procedury, * ponieważ nie wymaga wpisywania tylu parametrów (i odkładania ich * na stos/zdejmowania ze stosu) */ void UaktualnijIstniejaceUrzadzenie (char *sNazwaUrz, long nWartosc) { typUrzadzenie *urz; /* --- Nazwa nie może być pusta --- */ if (sNazwaUrz == NULL) return; /* --- Szukamy urządzenia według nazwy --- */ urz = SzukajUrzadzenia (sNazwaUrz); /* --- Jeśli urządzenie istnieje... --- */ if (urz) { /* --- Dodajemy wartość --- */
Część III Rysowanie, kolor i GDK
270 NowaWartosc (urz, nWartosc, FALSE); } }
/* * NowaWartosc * * Procedura dodaje wartość do listy wartości, przechowywanych * dla danego urządzenia. Musi także przesunąć wartości o 1 za * każdym razem, kiedy dodajemy kolejną wartość. * * urz - urządzenie, dla którego należy dodać wartość * wartosc - wartość dodawana dla urządzenia * bInicjuj - Czy jest to pierwsza dodawana wartość? */ void NowaWartosc (typUrzadzenie *urz, typStaraWartosc wartosc, int bInicjuj) { int i; /* --- Przesuwamy wartości w dół --- */ for (i = MAKS_WARTOSCI - 2; i >=0; i--) { urz->nWartosci[i+1] = urz->nWartosci[i]; } /* --- Jeśli to NIE JEST inicjacja --- */ if (!bInicjuj) { /* --- Dodajemy nową wartość na koniec --- */ if (urz->nTyp == PRZYROST) { /* --- Nowa wartość to przyrost, czyli różnica * pomiędzy poprzednią a bieżącą wartością */ urz->nWartosci[0] = wartosc - urz->nOstatnia; } else { /* --- Zapisujemy wartość tak, jak jest --- */ urz->nWartosci[0] = wartosc; } } /* --- Zapamiętujemy ostatnią wartość --- */
271 urz->nOstatnia = wartosc; /* --- Obliczamy wartość maksymalną --- */ if (urz->nWartosci[0] > urz->nMaks) { urz->nMaks = urz->nWartosci[0]; } }
sys.c Plik sys.c zawiera procedury, które analizują pliki /proc (w celu uzyskania informacji o obciążeniu sieci i procesora). Informacje o każdym urządzeniu są uaktualniane poprzez wywołania funkcji z pliku urzadzenie.c. Można łatwo dodać inne analizatory, posługując się istniejącym kodem jako przykładem. Procedura dodawania nowego urządzenia polega na odczytaniu informacji, zanalizowaniu jej i uaktualnieniu informacji o urządzeniu (przy pomocy funkcji z pliku urzadzenie.c). Można potem odwoływać się do urządzenia poprzez uniwersalną strukturę danych. /* * Plik: sys.c * Autor: Eric Harlow * */ #include #include #include #include "urzadzenie.h" #define SEPARATORY " |\n" void PobierzDaneOPakietach (); /* * PobierzDaneOPakietach * * Pobiera informacje o pakietach przesyłanych przez sieć * Ethernet. Dane o pakietach znajdują się w pliku /proc/net/dev, * jeśli komputer posiada system plików /proc. */ void PobierzDaneOPakietach () {
Część III Rysowanie, kolor i GDK
272 char *szNazwaPliku = "/proc/net/dev"; char szBufor[132]; int nNumerLinii = 0; FILE *wp; char *sLeksem; char *sNazwaUrz; int nNrSlowa; long nPrzychodzace; long nWychodzace; typUrzadzenie *urz;
/* --- Otwieramy plik, aby pobrać informacje --- */ wp = fopen (szNazwaPliku, "r"); if (wp) { /* --- Dopóki są dane do odczytania --- */ while (!feof (wp)) { nNumerLinii++; /* --- Wczytujemy linię tekstu --- */ fgets (szBufor, sizeof (szBufor), wp); /* -- Po trzeciej linii są informacje o urządzeniach -- */ if (nNumerLinii >= 3) { nNrSlowa = 0; sNazwaUrz = NULL; sLeksem = strtok (szBufor, SEPARATORY); while (sLeksem) { switch (nNrSlowa) { case 0: sNazwaUrz = sLeksem; break; case 1: nPrzychodzace = atoi (sLeksem); break; case 6: nWychodzace = atoi (sLeksem); break; }
273 nNrSlowa++; sLeksem = strtok (NULL, SEPARATORY); } /* --- Znaleźliśmy nazwę urządzenia --- */ if (sNazwaUrz) { /* --- Wyszukujemy urządzenie --- */ urz = SzukajUrzadzenie (sNazwaUrz); /* --- Dodajemy je/uaktualniamy dane --- */ if (urz) { UaktualnijIstniejaceUrzadzenie (sNazwaUrz, (long) (nPrzychodzace + nWychodzace)); } else { UaktualnijUrzadzenie (sNazwaUrz, (long) (nPrzychodzace + nWychodzace), PRZYROST, 1); } } szBufor[0] = (char) 0; } } /* --- Wszystko gotowe --- */ fclose (wp); } else { /* --- Niestety, błąd! --- */ printf ("Nie udało się otworzyć pliku %s.", szNazwaPliku); } } /* * PobierzDaneOCPU * * Odczytuje plik /proc/loadavg i analizuje zawarte w nim * informacje o procesorze. */ void PobierzDaneOCPU () { static char *szPlik = "/proc/loadavg"; char szBufor[88];
Część III Rysowanie, kolor i GDK
274 FILE *wp; float cpu1, cpu2, cpu3; long lcpu1, lcpu2, lcpu3; typUrzadzenie *urz; /* --- Otwieramy plik z danymi o CPU --- */ wp = fopen (szPlik, "r"); /* --- Jeśli otwarcie się powiodło... --- */ if (wp) { fgets (szBufor, sizeof (szBufor), wp);
/* --- Pobieramy liczby, określające obciążenie CPU --- */ sscanf (szBufor, "%f %f %f", &cpu1, &cpu2, &cpu3); /* --- Portrzebny zakres to 1-100+, a nie .01 to 1.0+ --- */ lcpu1 = cpu1 * 100; lcpu2 = cpu2 * 100; lcpu3 = cpu3 * 100; /* --- Wyszukujemy urządzenie --- */ urz = SzukajUrzadzenia ("cpu1"); if (urz) { /* --- Już istnieje, uaktualniamy urządzenie --- */ UaktualnijIstniejaceUrzadzenie ("cpu1", lcpu1); UaktualnijIstniejaceUrzadzenie ("cpu5", lcpu2); UaktualnijIstniejaceUrzadzenie ("cpu15", lcpu3); } else { /* --- To pierwszy raz; tworzymy urządzenia --- */ UaktualnijUrzadzenie ("cpu1", lcpu1, RZECZYWISTA, 100); UaktualnijUrzadzenie ("cpu5", lcpu2, RZECZYWISTA, 100); UaktualnijUrzadzenie ("cpu15", lcpu3, RZECZYWISTA, 100); } /* --- Sprzątamy. --- */ fclose (wp); } else { printf ("Nie udało się otworzyć pliku %s.\n", szPlik); } }
275 wykres.c W pliku tym znajduje się cały kod operujący na kontrolce obszaru rysunkowego. Aktualne dane sprawdzamy za pośrednictwem nazw urządzeń, używanych w sys.c. Funkcje UaktualnijUrzadzenie i UaktualnijIstniejaceUrzadzenie tworzą urządzenie i związane z nim wartości liczbowe. Procedury w pliku wykres.c pobierają te dane, wywołując funkcje z pliku urzadzenie.c. Ponieważ plik wykres.c nie ma żadnej wiedzy na temat systemu /proc, można zmienić szczegóły implementacyjne (sys.c), gdyby zaszły zmiany w jądrze (mało prawdopodobne), albo w razie przenoszenia aplikacji do systemu operacyjnego, który nie posiada systemu plików /proc. Wiele procedur przypomina te, które uaktualniały przykładowy zegar. Znaczącą różnicą jest jednak wykorzystanie kolorów podczas rysowania. Podczas uruchamiania aplikacji tworzymy kilka „piór” (są to w istocie struktury GdkGC), które pozwalają na rysowanie poszczególnych kolorów. Miejsce na pióra jest przydzielane na wstępie, aby przyspieszyć rysowanie (w przeciwnym przypadku musielibyśmy tworzyć nowy GdkGC przy każdym przerysowaniu ekranu). Pióra tworzymy w funkcji o nazwie PobierzPioro, dzięki czemu kod jest czytelniejszy, niż gdyby funkcja nosiła nazwę PobierzGdkGC. /* * Plik: wykres.c * Autor: Eric Harlow * * */ #include #include "urzadzenie.h" void RysujUrzadzenie (GtkWidget *obszar_rys, char *szName, GdkGC *pen, int bSrednia); int WcisnietoPrzyciskSiec (); int WcisnietoPrzyciskSiecSrednia (); int WcisnietoPrzyciskCPU15 (); int WcisnietoPrzyciskCPU5 (); int WcisnietoPrzyciskCPU1 (); void Przerysuj (); GtkWidget *obszar_rys; typedef struct {
Część III Rysowanie, kolor i GDK
276 GdkDrawable *piksmapa; GdkGC *gc; } typGrafika; static typGrafika *g; static GdkGC *czarnePioro = NULL; static GdkGC *czerwonePioro = NULL; static GdkGC *niebieskiePioro = NULL; static GdkGC *zielonePioro = NULL; static GdkGC *szarePioro = NULL;
/* * NowaGrafika * * Tworzy nowy element danych graficznych, przechowujący piksmapę * i kontekst gc. */ typGrafika *NowaGrafika () { typGrafika *gfx; /* --- Przydzielamy pamięć --- */ gfx = (typGrafika *) g_malloc (sizeof (typGrafika)); /* --- Inicjujemy --- */ gfx->gc = NULL; gfx->piksmapa = NULL; /* --- Zwracamy element gotowy do użycia --- */ return (gfx); } /* * PobierzPioro * * Zwraca pióro na podstawie przekazanej struktury GdkColor * Pióro (po prostu GdkGC) jest tworzone i zwracane w postaci * gotowej do użycia. */ GdkGC *PobierzPioro (GdkColor *c) {
277 GdkGC *gc; /* --- Tworzymy kontekst gc --- */ gc = gdk_gc_new (g->piksmapa); /* --- Ustawiamy kolor pierwszego planu --- */ gdk_gc_set_foreground (gc, c); /* --- Zwracamy kontekst gc --- */ return (gc); } /* * NowyKolor * * Tworzymy kolor na podstawie listy parametrów i przydzielamy * mu miejsce */ GdkColor *NowyKolor (long czerwony, long zielony, long niebieski) { /* --- Tworzymy strukturę koloru --- */ GdkColor *c = (GdkColor *) g_malloc (sizeof (GdkColor)); /* --- Wypełniamy ją --- */ c->red = czerwony; c->green = zielony; c->blue = niebieski; gdk_color_alloc (gdk_colormap_get_system (), c); return (c); } /* * UaktualnijIPrzerysuj * * Procedura sprawdza najnowsze statystyki obciążenia sieci * oraz procesora i uaktualnia ekran na podstawie tych * informacji */ gint UaktualnijIPrzerysuj (gpointer dane) { /* --- Pobieramy informacje o sieci --- */
Część III Rysowanie, kolor i GDK
278 PobierzDaneOPakietach ();
/* --- Pobieramy informacje o procesorze --- */ PobierzDaneOCPU (); /* --- Przerysowujemy ekran --- */ Przerysuj (); return (1); } /* * Przerysuj * * Uaktualnia ekran na podstawie najnowszych danych */ void Przerysuj () { GdkRectangle uakt_prostokat; /* --- czyścimy piksmapę, aby móc na niej rysować --- */ gdk_draw_rectangle (g->piksmapa, obszar_rys->style->white_gc, TRUE, 0, 0, obszar_rys->allocation.width, obszar_rys->allocation.height); /* --- Jeśli użytkownik chce widzieć rzeczywiste obciążenie sieci ... --- */ if (WcisnietoPrzyciskSiec ()) { RysujUrzadzenie (obszar_rys, "eth0:", szarePioro, 0); } /* -- Jeśli użytkownik chce widzieć średnie obciążenie sieci ... -- */ if (WcisnietoPrzyciskSiecSrednia ()) { RysujUrzadzenie (obszar_rys, "eth0:", czarnePioro, 1); } /* -- Jeśli użytkownik chce widzieć średnie obciążenie procesora w ostatnich 15 minutach ... -- */ if (WcisnietoPrzyciskCPU15 ()) { RysujUrzadzenie (obszar_rys, "cpu15", niebieskiePioro, 0); }
279 /* -- Jeśli użytkownik chce widzieć średnie obciążenie procesora w ostatnich 5 minutach ... -- */ if (WcisnietoPrzyciskCPU5 ()) { RysujUrzadzenie (obszar_rys, "cpu5", zielonePioro, 0); } /* -- Jeśli użytkownik chce widzieć średnie obciążenie procesora w ostatniej minucie ... -- */ if (WcisnietoPrzyciskCPU1 ()) { RysujUrzadzenie (obszar_rys, "cpu1", czerwonePioro, 0); } /* --- Uaktualniamy ekran drugoplanową piksmapą --- */ uakt_prostokat.x = 0; uakt_prostokat.y = 0; uakt_prostokat.width = obszar_rys->allocation.width; uakt_prostokat.height = obszar_rys->allocation.height; gtk_widget_draw (obszar_rys, &uakt_prostokat); } /* * RysujUrzadzenie * * Rysuje wykres z informacjami o urządzeniu * * obszar_rys - kontrolka * szNazwa - nazwa monitorowanego urządzenia * pioro - GC z informacjami o kolorze * bSrednia - Znacznik uśredniania. True => przeprowadzić uśrednianie */ void RysujUrzadzenie (GtkWidget *obszar_rys, char *szNazwa, GdkGC *pioro, int bSrednia) { typUrzadzenie *urz; int poprzx = 0; int poprzy = 0; int x = 0; int y = 0; int i; int nOstatnia; /* --- Wyszukujemy urządzenie na podstawie nazwy --- */
Część III Rysowanie, kolor i GDK
280 urz = SzukajUrzadzenia (szNazwa); /* --- Jeśli je znaleźliśmy --- */ if (urz) {
/* --- Jeśli należy wykonać uśrednienie --- */ if (bSrednia) { nOstatnia = MAKS_WARTOSCI-4; } else { nOstatnia = MAKS_WARTOSCI; } /* --- Rysujemy w poprzek kontrolki --- */ for (i = 0; i < obszar_rys->allocation.width && i < nOstatnia; i++) { x = i; if (urz->nMaks != 0) { if (bSrednia) { y = ((urz->nWartosci[i] + urz->nWartosci[i+1] + urz->nWartosci[i+2] + urz->nWartosci[i+3] + urz->nWartosci[i+4]) * obszar_rys->allocation.height) / (urz->nMaks * 5); } else { y = (urz->nWartosci[i] * obszar_rys->allocation.height) / urz->nMaks; } y = obszar_rys->allocation.height - y; } else { y = 1; } if (i == 0) { poprzx = x; poprzy = y; } /* --- Rysujemy linię od poprzedniego do bieżącego
281 --- punktu --- */ gdk_draw_line (g->piksmapa, pioro, poprzx, poprzy, x, y); /* --- Następnym "poprzednim punktem" będzie bieżący -- */ poprzx = x; poprzy = y; } } else { /* --- Nigdy nie powinno się zdarzyć! --- */ printf ("Wskaźnik do urządzenia to NULL (%s)\n", szNazwa); } } /* * configure_event * * Wywoływana podczas tworzenia obszaru rysunkowego * i za każdym razem, kiedy jego rozmiar ulegnie zmianie. * Tworzy nową piksmapę-bufor o odpowiednich rozmiarach. */ static gint configure_event (GtkWidget *kontrolka, GdkEventConfigure *zdarzenie) { if (g == NULL) { g = NowaGrafika (); } /* --- Zwalniamy piksmapę --- */ if (g->piksmapa) { gdk_pixmap_unref (g->piksmapa); } /* --- Tworzymy nową piksmapę --- */ g->piksmapa = gdk_pixmap_new (kontrolka->window, kontrolka->allocation.width, kontrolka->allocation.height, -1); /* --- Jeśli jeszcze nie utworzyliśmy piór... --- */
Część III Rysowanie, kolor i GDK
282 if (czarnePioro == NULL) {
/* --- ...tworzymy kolorowe pióra --- */ czarnePioro = PobierzPioro (NowyKolor (0, 0, 0)); czerwonePioro = PobierzPioro (NowyKolor (0xffff, 0, 0)); niebieskiePioro = PobierzPioro (NowyKolor (0, 0, 0xffff)); zielonePioro = PobierzPioro (NowyKolor (0, 0xffff, 0)); szarePioro = PobierzPioro (NowyKolor (0x9000, 0x9000, 0x9000)); } /* --- Czyścimy obszar rysunkowy --- */ gdk_draw_rectangle (g->piksmapa, kontrolka->style->white_gc, TRUE, 0, 0, kontrolka->allocation.width, kontrolka->allocation.height); return TRUE; } /* * expose_event * * Przerysowuje ekran przy pomocy piksmapy */ static gint expose_event (GtkWidget *kontrolka, GdkEventExpose *zdarzenie) { /* --- Kopiujemy piksmapę do okna --- */ gdk_draw_pixmap (kontrolka->window, kontrolka->style->fg_gc[GTK_WIDGET_STATE (kontrolka)], g->piksmapa, zdarzenie->area.x, zdarzenie->area.y, zdarzenie->area.x, zdarzenie->area.y, zdarzenie->area.width, zdarzenie->area.height); return FALSE; } /* * zamknij *
283 * Kończy działanie aplikacji */ void zamknij () { gtk_exit (0); } /* * UtworzObszarRysunkowy * * Tworzy kontrolkę obszaru rysunkowego i zwraca ją. */ GtkWidget *UtworzObszarRysunkowy () { /* --- Tworzymy obszar rysunkowy --- */ obszar_rys = gtk_drawing_area_new (); /* --- Nadajemy mu odpowiednie rozmiary --- */ gtk_drawing_area_size (GTK_DRAWING_AREA (obszar_rys), 200, 200); /* --- Uwidaczniamy go --- */ gtk_widget_show (obszar_rys); /* --- Musimy sprawdzaæ expose_event i configure_event --- */ gtk_signal_connect (GTK_OBJECT (obszar_rys), "expose_event", (GtkSignalFunc) expose_event, NULL); gtk_signal_connect (GTK_OBJECT(obszar_rys), "configure_event", (GtkSignalFunc) configure_event, NULL); /* --- Wywołujemy funkcję co 2 sekundy --- */ gtk_timeout_add (2000, UaktualnijIPrzerysuj, obszar_rys); /* --- Zwracamy kontrolkę, aby można było umieścić ją na ekranie --- */ return (obszar_rys); }
Podsumowanie Połączenie możliwości GDK z kontrolką obszaru rysunkowego umożliwia tworzenie skomplikowanych, zawierających grafikę aplikacji. GDK posiada procedury pozwalające na rysowanie prostych kształtów wewnątrz kontrolki obszaru rysunkowego. Wykorzystanie w aplikacjach
284
Część III Rysowanie, kolor i GDK
techniki podwójnego buforowania pomaga wyeliminować migotanie, choć wymaga większego nakładu pracy. Aplikacje wyświetlające informacje graficzne powinny zawsze używać podwójnego buforowania, aby usunąć migotanie obrazu.
Rozdział 11 Style, kolory, czcionki, wskaźniki myszy i referencje Style mają wpływ na wygląd rysowanej kontrolki, zwłaszcza na jej kolor i używaną czcionkę. Każdy styl składa się z kilku kolorów i jednej czcionki. Style są częścią GTK+, ale kolory i czcionki są częścią GDK. Chociaż obsługa kolorów jest zawarta w GDK, GTK+ posiada okno wyboru koloru, które można wykorzystać w swoich aplikacjach. Jeśli program tego wymaga, GTK+ dysponuje także oknem wyboru czcionek. Oba te okna dialogowe przypominają okno wyboru pliku i ujednolicają interfejs użytkownika w tych aplikacjach GTK+, które umożliwiają użytkownikom wybieranie koloru i czcionki. GDK pozwala także na zmianę wskaźnika myszy na jeden z wbudowanych kształtów albo na kształt zdefiniowany przez programistę. Referencje pomagają GTK+ w obsłudze używanych kontrolek. GTK+ zwalnia kontrolki, kiedy sądzi, że nie są już potrzebne, czemu w pewnych sytuacjach należy zapobiegać.
Style (kolory i czcionki) Każda kontrolka w GTK+ posiada styl, który określa jej wygląd na ekranie. Podstawowy styl składa się ze schematu kolorów i czcionki. Aplikacje GTK+ zazwyczaj dzielą typowy styl i właśnie dlatego wszystkie kontrolki mają te same kolory i korzystają z tej samej czcionki. Kontrolka może posiadać swój własny, niepowtarzalny styl, ale definiowanie odrębnego stylu dla każdej kontrolki jest raczej nie najlepszym pomysłem. Wyświetlanie wielu kontrolek o odmiennych stylach odciąga uwagę od samego programu, zwłaszcza wtedy, kiedy kontrolki mają różne kolory. Lepiej jest ograniczyć liczbę styli do najwyżej kilku (albo jednego), a zaoszczędzony czas przeznaczyć na zwiększenie funkcjonalności aplikacji. Chociaż w stylu określona jest tylko jedna czcionka, to może on mieć wiele kolorów. Kontrolka może znajdować się w jednym z kilku stanów:
288
Część III Rysowanie, kolor i GDK
NORMAL, ACTIVE, PRELIGHT, SELECTED i INSENSITIVE. Każdemu stanowi można przypisać odrębny schemat kolorystyczny. NORMAL
Zwykły sposób rysowania obiektu.
ACTIVE
Obiekt jest aktywny - w przypadku przycisku stan ACTIVE występuje wtedy, kiedy przycisk jest wciśnięty.
PRELIGHT
Zazwyczaj oznacza, że nad obiektem przesuwa się wskaźnik myszy. Kolor ten podpowiada użytkownikowi, że może kliknąć na obiekcie.
SELECTED
Obiekt został zaznaczony.
INSENSITIVE
Kontrolka jest niewrażliwa (nie można jej wybrać). W terminologii firmy Microsoft stan taki jest określany mianem nieaktywnego (disabled)
Każdy zdefiniowany zbiór kolorów wyraża stan, w którym znajduje się kontrolka. Można obejrzeć je w działaniu na przykładzie kontrolki przycisku. Przycisk zazwyczaj znajduje się w stanie NORMAL. Kiedy przesuniemy nad niego wskaźnik myszy, przycisk znajdzie się w stanie PRELIGHT i będzie wyświetlany w kolorach właściwych dla tego stanu. Jeśli klikniemy przycisk, zostanie on wyświetlony w kolorach zdefiniowanych dla stanu ACTIVE. Nie każda kontrolka może znajdować się we wszystkich stanach.
Kolory Kolory definiujemy jako strukturę GdkColor. Kolor posiada trzy tworzące go składowe: czerwoną, zieloną i niebieską (RGB). /* --- definiujemy kolor czerwony: dużo czerwonego, bez niebieskiego i --- zielonego */ GdkColor kolor = {0, 0xffff, 0x0000, 0x0000};
Po stworzeniu struktur GdkColor trzeba zażądać przydzielenia koloru z zasobów systemowych przy pomocy funkcji gdk_color_alloc. Funkcja ta próbuje dopasować zdefiniowany kolor do kolorów udostępnianych przez system. Funkcja przyjmuje mapę kolorów i kolor, ale jeśli korzystamy z niewielu kolorów, możemy przekazać jej systemową mapę kolorów, zwracaną przez funkcję gdk_colormap_get_system. Po przydzieleniu kolorów można wykorzystać je do tworzenia nowych, (oby) bardziej interesujących stylów.
Style, kolory, czcionki, wskaźniki myszy i referencje
289
Korzystanie ze stylów Można pobrać domyślny styl aplikacji przy pomocy funkcji gtk_widget_get_default_style. Bezpośrednia ingerencja w ten styl jest złą praktyką, ale możemy stworzyć jego kopię. Funkcja gtk_style_copy tworzy kopię stylu, którą można poddać dowolnym manipulacjom. Struktura GtkStyle zawiera tablicę kolorów pierwszoplanowych (foreground colors, fg), kolorów tła (background colors, bg) i kolorów tekstu (text colors, text), która definiuje kolory stylu. Aby zmodyfikować przycisk tak, żeby wyświetlał biały tekst na czarnym tle, możemy: /* --- zdefiniować kolory --- */ GdkColor bialy = {0, 0xffff, 0xffff, 0xffff); GdkColor czarny = {0, 0x0000, 0x0000, 0x0000); /* --- przydzielić kolory --- */ gdk_color_alloc (gdk_colormap_get_system (), &bialy); gdk_color_alloc (gdk_colormap_get_system (), &czarny); /* --- pobrać domyślny styl --- */ styl_domyslny = gtk_widget_get_default_style (); /* --- wykonać kopię stylu na własny użytek --- */ styl = gtk_style_copy (styl_domyslny); /* --- zmodyfikować kolory --- */ styl->fg[NORMAL] = bialy; styl->text[NORMAL] = bialy; styl->bg[NORMAL] = czarny;
Po utworzeniu stylu można zmieniać z jego pomocą wygląd dowolnej kontrolki. Funkcja gtk_widget_set_style ustawia styl istniejącej kontrolki, ale nie zmienia stanu żadnych jej potomków. Można skorzystać z funkcji gtk_container_foreach, aby zmienić styl kontrolki i wszystkich zawartych w niej kontrolek, na przykład w taki sposób: void UstawStylRekurencyjnie (GtkWidget *kontrolka, gpointer dane) { GtkStyle *styl; /* --- pobieramy styl --- */ styl = (GtkStyle *) dane; /* --- ustawiamy styl kontrolki --- */ gtk_widget_set_style (kontrolka, styl);
Część III Rysowanie, kolor i GDK
290
/* --- jeśli kontrolka może posiadać potomków --- */ if (GTK_IS_CONTAINER (kontrolka)) { /* --- ustawiamy także styl wszystkich potomków --- */ gtk_container_foreach (GTK_CONTAINER (kontrolka), UstawStylRekurencyjnie, styl); } }
Zamiast zmieniać styl kontrolek już po ich utworzeniu, można w łatwy sposób ustawić styl stosowany do nowo tworzonych kontrolek. Funkcja gtk_widget_push_style ustawia bieżący styl dla wszystkich tworzonych kontrolek. Każda kontrolka, utworzona po wywołaniu funkcji gtk_widget_push_style, otrzymuje styl odłożony na stos. Kiedy styl nie jest już potrzebny, należy zdjąć go ze stosu funkcją gtk_widget_pop_style. Ponieważ style są umieszczone na stosie, wiele stylów może być zdefiniowanych jednocześnie, ale używać możemy tylko stylu z wierzchołka stosu. Jeśli przy pomocy funkcji gtk_widget_pop_style zdejmiemy ze stosu najwyżej położony styl, wówczas kontrolki będą tworzone przy użyciu następnego w kolejności stylu. Kiedy styl kontrolki ulegnie zmianie, otrzyma ona sygnał "style_set". Poniższy przykład pokazuje, w jaki sposób można zmieniać kolor przycisków za pomocą stylów i jak ustawiać domyślny styl nowo tworzonych kontrolek. Program wyświetla sześć przycisków - pierwsze cztery są umieszczone wewnątrz okna dialogowego o określonym schemacie kolorystycznym (wskazówka: schemat „Boże Narodzenie” jest zielono-czerwony). Piąty przycisk sprawia, że wszystkie przyciski zmieniają kolory. W szóstym przycisku zmodyfikowane są wszystkie style, więc od początku jest wyświetlany w innych kolorach; jeśli przesuniemy nad niego wskaźnik myszy, zobaczymy, że w stanie PRELIGHT jest żółty - inaczej, niż w domyślnym schemacie (po kliknięciu przycisku „Przyciski szaleją” początkowy styl zostanie zastąpiony innym). Nie umieszczamy tutaj kodu funkcji WywolajOkno, która jest włączona do innego pliku. Otwiera ona okno dialogowe i wyświetla komunikat (patrz rysunek 11.1). Można rzucić okiem na jej kod źródłowy, ale zasadnicza część programu znajduje się poniżej.
Style, kolory, czcionki, wskaźniki myszy i referencje
Rysunek 11.1. Okno dialogowe modyfikujące kolory i rezultaty modyfikacji. /* * Plik: przycisk.c * Autor: Eric Harlow * * Pokazuje sposób modyfikowania stylów - zwłaszcza kolorów * * --- NORMAL, PRELIGHT, ACTIVE, INSENSITIVE, SELECTED */ #include #include GtkWidget *przycisk; GtkStyle *stylCzerwony; /* * --- Definicje kolorów */ GdkColor czerwony = {0, 0xffff, 0x0000, 0x0000}; GdkColor niebieski = {0, 0x0000, 0x0000, 0xffff}; GdkColor zielony = {0, 0x0000, 0xffff, 0x0000}; GdkColor zolty = {0, 0xffff, 0xffff, 0x0000}; GdkColor fioletowy = {0, 0xffff, 0x0000, 0xffff}; GdkColor pomaranczowy = {0, 0xffff, 0x9999, 0x0000}; GdkColor turkusowy = {0, 0x0000, 0xffff, 0xffff}; GdkColor czarny = {0, 0x0000, 0x0000, 0x0000}; GdkColor bialy = {0, 0xffff, 0xffff, 0xffff}; /* --- Tworzymy listę kolorów, z której będzie korzystał * przycisk przypisujący losowe kolory */ GdkColor listakolorow[] = { {0, 0xffff, 0x0000, 0x0000}, {0, 0x0000, 0x0000, 0xffff},
291
Część III Rysowanie, kolor i GDK
292 {0, {0, {0, {0, {0, {0, {0,
0x0000, 0xffff, 0xffff, 0xffff, 0x0000, 0x0000, 0xffff,
0xffff, 0xffff, 0x0000, 0x9999, 0xffff, 0x0000, 0xffff,
0x0000}, 0x0000}, 0xffff}, 0x0000}, 0xffff}, 0x0000}, 0xffff}
}; /* --- Obliczamy liczbę kolorów --- */ static int liczbaKolorow = sizeof (listakolorow) / sizeof (GdkColor); /* --- Tworzymy style okien dialogowych --- */ GtkStyle *stylBozeNarodzenie; GtkStyle *stylWszystkichSwietych; GtkStyle *stylSwietyPatryk; GtkStyle *stylDzienNiepodleglosci; /* --- pionowe pole pakujące na przyciski --- */ GtkWidget *ypole; /* * UtworzKolorowyStyl * * Tworzy styl na podstawie przekazanych kolorów. * Ustawia kolor pierwszoplanowy, kolor tła i kolor * tekstu. Zwróćmy uwagę, że wszystkie stany kontrolki * będą posiadały te same kolory. * * kp - kolor pierwszoplanowy * tekst - kolor tekstu * kt - kolor tła */ GtkStyle *UtworzKolorowyStyl (GdkColor kp, GdkColor tekst, GdkColor kt) { GtkStyle *styl_dom; GtkStyle *styl; int i; /* --- Pobieramy styl domyślny --- */
Style, kolory, czcionki, wskaźniki myszy i referencje
styl_dom = gtk_widget_get_default_style (); /* --- Tworzymy kopię --- */ styl = gtk_style_copy (styl_dom); /* --- Ustawiamy kolory dla każdego stanu --- */ for (i = 0; i < 5; i++) { /* --- Ustawiamy kolory dla stylu --- */ styl->fg[i] = kp; styl->text[i] = tekst; styl->bg[i] = kt; } /* --- Gotowe, oto nowy styl --- */ return (styl); } /* * StylLosowy * * Tworzy losowy styl na podstawie zdefiniowanej wyżej * listy kolorów. Nie sprawdza, czy dobór kolorów jest * właściwy (np. czy kolor pierwszoplanowy nie jest taki * sam jak kolor tła). */ GtkStyle *StylLosowy () { GtkStyle *styl; /* --- Tworzymy styl z losowymi kolorami --- */ styl = UtworzKolorowyStyl ( listakolorow[random () % liczbaKolorow], listakolorow[random () % liczbaKolorow], listakolorow[random () % liczbaKolorow]); return (styl); } /* * UtworzOdjazdoweKolory * * Tworzy style dla przycisków. Każdy styl posiada inny zestaw
293
294
Część III Rysowanie, kolor i GDK
* kolorów, które będą przypisane do przycisków. */ void UtworzOdjazdoweKolory () { /* --- Czerwony na zielonym --- */ stylBozeNarodzenie = UtworzKolorowyStyl (czerwony, czerwony, zielony); /* --- Pomarańczowy na czarnym --- */ stylWszystkichSwietych = UtworzKolorowyStyl (pomaranczowy, pomaranczowy, czarny); /* --- Zielony na białym --- */ stylSwietyPatryk = UtworzKolorowyStyl (zielony, zielony, bialy); /* --- Czerwony i biały na niebieskim --- */ stylDzienNiepodleglosci = UtworzKolorowyStyl (czerwony, bialy, niebieski); } /* * ZamknijApl * * Wychodzimy z GTK. */ void ZamknijApl (GtkWidget *kontrolka, gpointer gdane) { gtk_main_quit (); } /* * Kliknięto przycisk * * procedura obsługi zdarzenia, wywoływana po kliknięciu przycisku */ void KliknietoPrzycisk (GtkWidget *kontrolka, gpointer gdane) { GtkStyle *styl; /* --- Pobieramy styl z parametru funkcji zwrotnej --- */ styl = (GtkStyle *) gdane;
Style, kolory, czcionki, wskaźniki myszy i referencje
/* --- Odkładamy na stosie nowy, kolorowy styl - teraz staje się stylem domyślnym --- */ gtk_widget_push_style (styl); /* --- Pokazujemy okno dialogowe w nowym stylu --- */ WywolajOkno (kontrolka, (gpointer) 2); /* --- Usuwamy styl --- */ gtk_widget_pop_style (); } /* * UstawionoStyl * * Wyświetla komunikat po każdej modyfikacji stylu. */ void UstawionoStyl (GtkWidget *kontrolka, gpointer dane) { printf ("Ustawiono styl\n"); } /* * UstawStylRekurencyjnie * * Ustawia styl kontrolki na styl otrzymany w parametrze "dane" * i zapewnia, że wszyscy potomkowie kontrolki (jeśli kontrolka * jest pojemnikiem) także będą ustawieni na ten styl. */ void UstawStylRekurencyjnie (GtkWidget *kontrolka, gpointer dane) { GtkStyle *styl; /* --- pobieramy styl --- */ styl = (GtkStyle *) dane; /* --- ustawiamy styl kontrolki --- */ gtk_widget_set_style (kontrolka, styl); /* --- jeśli kontrolka może posiadać potomków --- */ if (GTK_IS_CONTAINER (kontrolka)) { /* --- ustawiamy także styl wszystkich potomków --- */ gtk_container_foreach (GTK_CONTAINER (kontrolka),
295
Część III Rysowanie, kolor i GDK
296
UstawStylRekurencyjnie, styl); } } /* * SzalonyPrzycisk * * Ustawia kolory przycisku na kolory dobrane losowo * ze zdefiniowanej wcześniej tabeli. */ void SzalonyPrzycisk (GtkWidget *kontrolka, gpointer dane) { GtkStyle *styl; /* --- Wybieramy losowy styl --- */ styl = StylLosowy (); /* --- Ustawiamy styl kontrolki --- */ UstawStylRekurencyjnie (kontrolka, (gpointer) styl); } /* * PokolorujPrzyciski * * Ustawia kolory kontrolek potomnych na losowy styl. */ void PokolorujPrzyciski (GtkWidget *kontrolka, gpointer dane) { /* --- Zmieniamy styl wszystkich potomków --- */ gtk_container_foreach (GTK_CONTAINER (ypole), SzalonyPrzycisk, NULL); } /* * UtworzPrzyciskKolorowegoOkna * * Tworzy przycisk w pionowym polu pakującym. Styl będzie * przekazywany w funkcji zwrotnej dla przycisku, aby okno * dialogowe, otwierane po naciśnięciu przycisku, było tworzone * w tym stylu. */ void UtworzPrzyciskKolorowegoOkna (GtkWidget *ypole, char *etykieta,
Style, kolory, czcionki, wskaźniki myszy i referencje
297
GtkStyle *styl) { GtkWidget *przycisk; /* --- Tworzymy przycisk z etykietą --- */ przycisk = gtk_button_new_with_label (etykieta); /* --- Konfigurujemy styl w funkcji zwrotnej "clicked" --- */ gtk_signal_connect (GTK_OBJECT (przycisk), "clicked", GTK_SIGNAL_FUNC (KliknietoPrzycisk), styl);
(gpointer)
/* --- Konfigurujemy styl w funkcji zwrotnej "style_set" --- */ gtk_signal_connect (GTK_OBJECT (przycisk), "style_set", GTK_SIGNAL_FUNC (UstawionoStyl), (gpointer) styl); /* --- Umieszczamy przycisk w pionowym polu pakującym --- */ gtk_box_pack_start (GTK_BOX (ypole), przycisk, FALSE, FALSE, 0); /* --- Uwidaczniamy przycisk --- */ gtk_widget_show (przycisk); } /* * UtworzPrzyciskKolorujacy * * Tworzy przycisk, który zmienia kolory wszystkich przycisków */ void UtworzPrzyciskKolorujacy (GtkWidget *ypole, char *etykieta) { GtkWidget *przycisk; /* --- Tworzymy przycisk --- */ przycisk = gtk_button_new_with_label (etykieta); /* --- Ustawiamy obsługę sygnału --- */ gtk_signal_connect (GTK_OBJECT (przycisk), "clicked", GTK_SIGNAL_FUNC (PokolorujPrzyciski), NULL); /* --- Umieszczamy przycisk w polu pakującym --- */ gtk_box_pack_start (GTK_BOX (ypole), przycisk, FALSE, FALSE, 0); /* --- Uwidaczniamy przycisk --- */ gtk_widget_show (przycisk);
Część III Rysowanie, kolor i GDK
298 }
/* * UtworzPrzyciskBlyskajacy * */ void UtworzPrzyciskBlyskajacy (GtkWidget *ypole, char *etykieta) { GtkStyle *styl_dom; GtkStyle *styl; styl_dom = gtk_widget_get_default_style (); styl = gtk_style_copy (styl_dom); styl->fg[GTK_STATE_NORMAL] = fioletowy; styl->text[GTK_STATE_NORMAL] = fioletowy; styl->bg[GTK_STATE_NORMAL] = turkusowy; styl->fg[GTK_STATE_PRELIGHT] = zielony; styl->text[GTK_STATE_PRELIGHT] = zielony; styl->bg[GTK_STATE_PRELIGHT] = niebieski; styl->fg[GTK_STATE_ACTIVE] = pomaranczowy; styl->text[GTK_STATE_ACTIVE] = pomaranczowy; styl->bg[GTK_STATE_ACTIVE] = zolty; gtk_widget_push_style (styl); /* --- Tworzymy nowy przycisk --- */ przycisk = gtk_button_new_with_label (etykieta); /* --- Umieszczamy przycisk w polu pakującym --- */ gtk_box_pack_start (GTK_BOX (ypole), przycisk, FALSE, FALSE, 0); /* --- Uwidaczniamy przycisk --- */ gtk_widget_show (przycisk); /* --- Usuwamy styl, aby nie był stylem domyślnym --- */ gtk_widget_pop_style (); } /* * main *
Style, kolory, czcionki, wskaźniki myszy i referencje
* Tutaj zaczyna się program */ int main (int argc, char *argv[]) { GtkWidget *okno; /* --- Inicjacja GTK, obsługa parametrów wiersza polecenia --- */ gtk_init (&argc, &argv); /* --- Tworzymy okno gtk - na razie NIE JEST widoczne --- */ okno = gtk_window_new (GTK_WINDOW_TOPLEVEL); gtk_signal_connect (GTK_OBJECT (okno), "destroy", GTK_SIGNAL_FUNC (ZamknijApl), NULL); /* --- Trochę przestrzeni wokół obiektów w pojemniku --- */ gtk_container_border_width (GTK_CONTAINER (okno), 15); ypole = gtk_vbox_new (FALSE, 0); /* --- Przydzielamy kolory --- */ PrzydzielKolory (); /* --- Przydzielamy style --- */ UtworzOdjazdoweKolory (); /* --- Tworzymy przyciski --- */ UtworzPrzyciskKolorowegoOkna (ypole, "Boże Narodzenie", stylBozeNarodzenie); UtworzPrzyciskKolorowegoOkna (ypole, "Dzień Św. Patryka", stylSwietyPatryk); UtworzPrzyciskKolorowegoOkna (ypole, "Wszystkich Świętych", stylWszystkichSwietych); UtworzPrzyciskKolorowegoOkna (ypole, "Dzień Niepodległości", stylDzienNiepodleglosci); UtworzPrzyciskKolorujacy (ypole, "Szalone Przyciski"); UtworzPrzyciskBlyskajacy (ypole, "Błyskający Przycisk"); /* --- Teraz uwidaczniamy okno --- */ gtk_widget_show (ypole); gtk_container_add (GTK_CONTAINER (okno), ypole); gtk_widget_show (okno);
299
Część III Rysowanie, kolor i GDK
300
/* --- Pętla zdarzeń w gtk. Czekamy na zakończenie programu --- */ gtk_main (); /* --- Zwracamy kod wyjściowy --- */ return 0; }
Okno dialogowe wyboru koloru Okno wyboru koloru udostępnia użytkownikom standardową metodę wybierania kolorów, podobnie jak okno wyboru pliku jest standardowym interfejsem, służącym do wybierania plików. Okno wyboru kolorów zawiera kilka kontrolek, które pomagają użytkownikowi w wybieraniu kolorów (patrz rysunek 11.2).
Rysunek 11.2. Kontrolka wyboru koloru.
Okno wyboru koloru tworzymy przy pomocy funkcji gtk_color_ selection_dialog_new. Przyciskom OK i Cancel możemy przypisać funkcje zwrotne, które będą obsługiwać zdarzenia związane z przyciskami. /* --- nowe okno dialogowe --- */ okno = gtk_color_selection_dialog_new ("Okno wyboru koloru"); /* --- chcemy wiedzieć, czy użytkownik nacisnął OK --- */ gtk_signal_connect ( GTK_OBJECT (GTK_COLOR_SELECTION_DIALOG (okno)->ok_button), "clicked", GTK_SIGNAL_FUNC (KliknietoPrzyciskOK), daneKoloru); /* --- usuwamy okno dialogowe po kliknięciu Cancel --- */ gtk_signal_connect (
Style, kolory, czcionki, wskaźniki myszy i referencje
GTK_OBJECT (GTK_COLOR_SELECTION_DIALOG >cancel_button), "clicked", GTK_SIGNAL_FUNC (gtk_widget_destroy), GTK_OBJECT(okno));
301 (okno)-
Sygnał color_changed informuje, że zmienił się kolor w oknie dialogowym. Można określić założenia, dotyczące wysyłania komunikatów o zmianie koloru, za pomocą funkcji gtk_color_selection_set_update_policy. Założenia te to GTK_UPDATE_CONTINUOUS, GTK_UPDATE_DISCONTINUOUS i GTK_ UPDATE_DELAYED. wysyła uaktualnienia w sposób ciągły, się koloru w oknie dialogowym. GTK_UPDATE_DISCONTINUOUS wysyła uaktualnienie po zwolnieniu klawisza myszy. GTK_UPDATE_ DELAYED wysyła uaktualnienie po zwolnieniu klawisza myszy, albo wtedy, kiedy wskaźnik myszy zatrzyma się na pewien czas w kręgu kolorów. GTK_UPDATE_CONTINUOUS
w miarę
zmieniania
/* --- chcemy wiedzieć, kiedy zmieni się kolor --- */ gtk_signal_connect ( GTK_OBJECT (GTK_COLOR_SELECTION_DIALOG (okno)->colorsel), "color_changed", GTK_SIGNAL_FUNC (ZmienilSieKolor), okno); /* --- chcemy być natychmiast informowani o zmianie koloru --- */ /* --- zostaniemy zalani komunikatami, ale mamy szybki komputer --- */ gtk_color_selection_set_update_policy ( GTK_OBJECT (GTK_COLOR_SELECTION_DIALOG (okno)->colorsel), GTK_UPDATE_CONTINUOUS);
Kiedy procedura obsługi zdarzenia chce poznać bieżący kolor w oknie dialogowym, musi przeprowadzić kilka czynności. Najpierw musi pobrać pole colorsel ze struktury okna dialogowego. Następnie musi uzyskać tablicę wartości kolorów (zdefiniowanych jako gdouble). Wartości te mają zakres 0...1, więc należy je przekształcić do zakresu 0...0xffff, mnożąc je przez 0xffff. Oprócz zdefiniowanych kolorów, zwrócona wartość może zawierać także przezroczystość (opacity) koloru. Można skonfigurować przezroczystość przy pomocy funkcji gtk_color_selection_ set_opacity, która spowoduje utworzenie dodatkowych kontrolek w oknie wyboru koloru, umożliwiających wybór przezroczystości. Przezroczystość jest zdefiniowana jako czwarta wartość w tablicy. GtkColorSelection *wybKoloru;
302
Część III Rysowanie, kolor i GDK
gdouble *daneKoloru[4]; /* --- pobieramy okno koloru, aby sprawdzić wybrany kolor --- */ wybKoloru = GTK_COLOR_SELECTION (okno->colorsel); /* --- pobieramy wartości koloru z kontrolki --- */ gtk_color_slection_get_color (wybKoloru, daneKoloru); /* --- "kolor" to struktura GdkColor --- */ kolor->red = daneKoloru[0] * 0xffff; kolor->green = daneKoloru[1] * 0xffff; kolor->blue = daneKoloru[2] * 0xffff; /* --- Przezroczystość znajduje się w daneKoloru[3], jeśli ustawiliśmy ją w oknie dialogowym --- */ /* --- Kolor został pobrany i jest gotowy do użycia --- */
Możemy zilustrować omówione zagadnienia, używając okna wyboru koloru do zmiany koloru kontrolki. Poniższy program umożliwia użytkownikowi zmianę koloru przycisku, poprzez kliknięcie przycisku i wybranie nowego koloru z okna dialogowego. Po kliknięciu przycisku OK styl przycisku zostanie zmodyfikowany tak, aby uwzględniał nowy kolor. Przykład składa się z dwóch plików - kolorowyprz.c i kolordiag.c.
kolorowyprz.c Plik kolorowyprz.c tworzy główne okno, steruje przyciskami i zmienia kolory. /* * Plik: kolorowyprz.c * */ #include #include GtkWidget *przycisk; GtkWidget *ypole; /* * UtworzStylTla * * Tworzy styl na podstawie przekazanych kolorów * Ustawia nowy kolor tła. Zwróćmy uwagę, że wszystkie
Style, kolory, czcionki, wskaźniki myszy i referencje
* stany kontrolki będą posiadały ten sam kolor. * * kt - kolor tła */ GtkStyle *UtworzStylTla (GdkColor kt) { GtkStyle *styl_dom; GtkStyle *styl; int i; /* --- Pobieramy styl domyślny --- */ styl_dom = gtk_widget_get_default_style (); /* --- Tworzymy jego kopię --- */ styl = gtk_style_copy (styl_dom); /* --- Ustawiamy kolory dla każdego stanu --- */ for (i = 0; i < 5; i++) { /* --- Ustawiamy kolory dla stanu --- */ styl->bg[i] = kt; } /* --- Gotowe, oto nowy styl --- */ return (styl); } /* * NowyStyl * */ GtkStyle *NowyStyl (GdkColor k) { GtkStyle *styl; /* --- Tworzymy nowy styl na podstawie koloru --- */ styl = UtworzStylTla (k); /* --- Zwracamy styl --- */ return (styl); } /* * ZamknijOknoApl
303
Część III Rysowanie, kolor i GDK
304
* * Zamyka aplikację */ gint ZamknijOknoApl (GtkWidget *kontrolka, gpointer gdane) { g_print ("Kończę pracę...\n"); gtk_main_quit (); return (FALSE); } /* * UstawStylRekurencyjnie * Ustawia styl kontrolki i wszystkich jej potomków */ void UstawStylRekurencyjnie (GtkWidget *kontrolka, gpointer dane) { GtkStyle *styl; /* --- pobieramy styl --- */ styl = (GtkStyle *) dane; /* --- ustawiamy styl kontrolki --- */ gtk_widget_set_style (kontrolka, styl); /* --- jeśli kontrolka może posiadać potomków --- */ if (GTK_IS_CONTAINER (kontrolka)) { /* --- ustawiamy także styl wszystkich potomków --- */ gtk_container_foreach (GTK_CONTAINER (kontrolka), UstawStylRekurencyjnie, styl); } } /* * KliknietoPrzycisk * * Procedura obsługi zdarzenia, wywoływana po kliknięciu przycisku. * Możemy jej użyć, ponieważ okno wyboru kolorów jest tutaj modalne, * i nie oddaje sterowania, dopóki nie zostanie pobrany kolor */ void KliknietoPrzycisk (GtkWidget *kontrolka, gpointer gdane) { GtkStyle *styl;
Style, kolory, czcionki, wskaźniki myszy i referencje
GdkColor kolor; /* --- Wywołujemy okno dialogowe, aby pobrać kolor --- */ PobierzKolorZOkna (&kolor); /* --- Tworzymy styl na podstawie koloru --- */ styl = NowyStyl (kolor); /* --- Ustawiamy nowy styl kontrolki */ UstawStylRekurencyjnie (kontrolka, (gpointer) styl); } /* * UtworzPrzycisk * * Tworzy przycisk i dodaje go do pionowego pola pakującego. * Ustawia także procedurę obsługi zdarzenia na "KliknietoPrzycisk" */ void UtworzPrzycisk (GtkWidget *ypole, char *etykieta) { GtkWidget *przycisk; /* --- Tworzymy przycisk --- */ przycisk = gtk_button_new_with_label (etykieta); gtk_signal_connect (GTK_OBJECT (przycisk), "clicked", GTK_SIGNAL_FUNC (KliknietoPrzycisk), NULL); /* --- Umieszczamy przycisk w polu pakującym --- */ gtk_box_pack_start (GTK_BOX (ypole), przycisk, FALSE, FALSE, 0); /* --- Uwidaczniamy przycisk --- */ gtk_widget_show (przycisk); } /* * main * * Tutaj zaczyna się program */ int main (int argc, char *argv[]) { GtkWidget *okno;
305
Część III Rysowanie, kolor i GDK
306
/* --- Inicjacja gtk, obsługa parametrów wiersza polecenia --- */ gtk_init (&argc, &argv); /* --- Tworzymy okno --- */ okno = gtk_window_new (GTK_WINDOW_TOPLEVEL); /* --- Musimy wiedzieć, kiedy okno będzie zamykane --- */ gtk_signal_connect (GTK_OBJECT (okno), "delete_event", GTK_SIGNAL_FUNC (ZamknijOknoApl), NULL); /* --- Trochę miejsca wokół obiektów w pojemniku --- */ gtk_container_border_width (GTK_CONTAINER (okno), 15); /* --- Tworzymy pionowe pole pakujące --- */ ypole = gtk_vbox_new (FALSE, 0); /* --- Tworzymy przycisk --- */ UtworzPrzycisk (ypole, "Wybierz kolor"); /* --- Teraz uwidaczniamy okno --- */ gtk_widget_show (ypole); gtk_container_add (GTK_CONTAINER (okno), ypole); gtk_widget_show (okno); /* --- Nie wracamy aż do zamknięcia aplikacji --- */ gtk_main (); /* --- Kod wyjściowy --- */ return 0; }
kolordiag.c Plik kolordiag.c wyświetla okno wyboru koloru i zwraca wybrany kolor. /* * Plik: kolordiag.c * * Wyświetla okno wyboru koloru, w którym użytkownik * może określić kolor przycisku. */ #include /* * Struktura umożliwiająca zrezygnowanie ze zmiennych globalnych
Style, kolory, czcionki, wskaźniki myszy i referencje
*/ typedef struct { GtkWidget *dialog; GdkColor *kolor; } typDaneZOknaKolorow; /* * ZamknijDialog * * Zamyka modalne okno dialogowe przy pomocy funkcji * gtk_main_quit, ponieważ uczyniono je modalnym przy pomocy * funkcji gtk_main. Zwalnia także pamięć. */ void ZamknijDialog (GtkWidget *w, typDaneZOknaKolorow *di) { gtk_main_quit (); gtk_grab_remove (w); g_free (di); } /* * KliknietoPrzyciskOK * * Pobiera kolor zaznaczony w oknie dialogowym i wydobywa * jego składowe rgb. Ustawia kolor GdkColor w strukturze * daneKolorow na te wartości */ void KliknietoPrzyciskOK (GtkWidget *k, typDaneZOknaKolorow *daneKolorow) { GtkColorSelectionDialog *wk; GtkColorSelection *wyb_kolor; gdouble kolor[4]; /* --- Pobieramy okno dialogowe i kolor --- */ wk = (GtkColorSelectionDialog *) daneKolorow->dialog; wyb_kolor= GTK_COLOR_SELECTION (wk->colorsel); /* --- Pobieramy kolor i przypisujemy go do GdkColor --- */ gtk_color_selection_get_color (wyb_kolor, kolor); daneKolorow->kolor->red = kolor[0] * 0xffff;
307
Część III Rysowanie, kolor i GDK
308
daneKolorow->kolor->green = kolor[1] * 0xffff; daneKolorow->kolor->blue = kolor[2] * 0xffff; /* --- Usuwamy okno dialogowe --- */ gtk_widget_destroy (GTK_WIDGET (wk)); } /* * ZmienilSieKolor * * Funkcja powiadamiana o zmianie koloru w oknie dialogowym */ void ZmienilSieKolor(GtkWidget *w, GtkColorSelectionDialog *wk) { GtkColorSelection *wyb_kolor; gdouble kolor[4]; /* --- Pobieramy kolor z zaznaczenia --- */ wyb_kolor = GTK_COLOR_SELECTION (wk->colorsel); gtk_color_selection_get_color (wyb_kolor, kolor); g_print ("Zmienił się kolor!\n"); } /* * PobierzKolorZOkna * * Wyświetla modalne okno dialogowe i umożliwia użytkownikowi * wybranie koloru, który zostanie zwrócony. */ void PobierzKolorZOkna (GdkColor *kolor) { static GtkWidget *okno = NULL; typDaneZOknaKolorow *daneKolorow; /* --- Nowe okno dialogowe --- */ okno = gtk_color_selection_dialog_new ("Okno wyboru koloru"); /* --- Przydzielamy pamięć na strukturę daneKolorow i wypełniamy jej pola --- */ daneKolorow = g_malloc (sizeof (typDaneZOknaKolorow)); daneKolorow->dialog = okno; daneKolorow->kolor = kolor;
Style, kolory, czcionki, wskaźniki myszy i referencje
309
gtk_color_selection_set_opacity (GTK_COLOR_SELECTION ( GTK_COLOR_SELECTION_DIALOG (okno)->colorsel), TRUE); gtk_color_selection_set_update_policy (GTK_COLOR_SELECTION ( GTK_COLOR_SELECTION_DIALOG (okno)->colorsel), GTK_UPDATE_CONTINUOUS); /* --- Chcemy być informowani o usunięciu okna --- */ gtk_signal_connect (GTK_OBJECT (okno), "destroy", GTK_SIGNAL_FUNC (ZamknijDialog), daneKolorow); /* --- Chcemy być informowani o zmianie kolorów --- */ gtk_signal_connect (GTK_OBJECT ( GTK_COLOR_SELECTION_DIALOG (okno)->colorsel), "color_changed", GTK_SIGNAL_FUNC (ZmienilSieKolor), okno); /* --- Chcemy być informowani o naciśnięciu przycisku OK --- */ gtk_signal_connect (GTK_OBJECT ( GTK_COLOR_SELECTION_DIALOG (okno)->ok_button), "clicked", GTK_SIGNAL_FUNC (KliknietoPrzyciskOK), daneKolorow); /* --- Usuwamy okno dialogowe po kliknięciu Cancel --- */ gtk_signal_connect_object (GTK_OBJECT ( GTK_COLOR_SELECTION_DIALOG (okno)->cancel_button), "clicked", GTK_SIGNAL_FUNC(gtk_widget_destroy), GTK_OBJECT (okno)); /* --- Pokazujemy okno --- */ gtk_widget_show (okno); /* --- Przechwytujemy sterowanie --- */ gtk_grab_add (okno); /* --- To sprawi, że okno będzie modalne, aż do wywołania gtk_main_quit --- */ gtk_main (); }
Część III Rysowanie, kolor i GDK
310 Czcionki
Czcionki można modyfikować w sposób zbliżony do koloru, ale we wszystkich stanach kontrolki istnieje tylko jedna czcionka. Czcionkę można załadować używając jej nazwy albo przy pomocy okna wyboru czcionki (patrz rysunek 11.3). Jeśli chcemy użyć nowej czcionki nie korzystając z okna wyboru czcionki, musimy znać jej nazwę i użyć funkcji gtk_font_load. Po załadowaniu czcionki można odpowiednio ustawić pole czcionki w strukturze stylu. /* --- ładujemy czcionkę --- */ czcionka = gdk_font_load (szCzcionka); /* --- przypisujemy czcionkę do stylu --- */ styl->font = czcionka;
Rysunek 11.3. Okno wyboru czcionki.
Trzeba więc znaleźć sposób ustalenia nazw czcionek w systemie. Aby wyświetlić wszystkie dostępne w systemie czcionki, można użyć funkcji XListFonts z biblioteki Xlib. Funkcja ta (oprócz innych parametrów) przyjmuje wzorzec, który określa, jakie czcionki zostaną zwrócone w liście nazw czcionek. Aby funkcja zwróciła wszystkie dostępne czcionki, używamy wzorca -*. Gwiazdka * pasuje do dowolnego tekstu, a więc także do dowolnej nazwy czcionki. Nazwy czcionek przypominają te umieszczone poniżej (są to niektóre spośród czcionek, zainstalowanych w systemie autora): -adobe-courier-medium-r-normal--8-80-75-75-m-50-iso8859-1
Style, kolory, czcionki, wskaźniki myszy i referencje
311
-adobe-helvetica-bold-o-normal--10-100-75-75-p-60-iso8859-1 -daewoo-gothic-medium-r-normal--16-120-100-100-c-160-ksc5601.1987-0 -daewoo-mincho-medium-r-normal--24-170-100-100-c-240-ksc5601.1987-0 -isas-fangsong ti-medium-r-normal--16-160-72-72-c-160-gb2312.1980-0 -isas-song ti-medium-r-normal--24-240-72-72-c-240-gb2312.1980-0 -jis-fixed-medium-r-normal--24-230-75-75-c-240-jisx0208.1983-0 -misc-fixed-bold-r-normal--13-120-75-75-c-80-iso8859-1 -adobe-times-medium-i-normal--12-120-75-75-p-63-iso8859-1
Poniższy krótki program wyświetla wszystkie czcionki w systemie: /* * Plik: PokazCzcionki.c * * Wyświetla wszystkie obecne w systemie czcionki * * Trzeba go uruchomić z terminala X */ #include #include #include /* * Jeśli użytkownicy mają więcej fontów, niż widać poniżej, * to trudno; to i tak sporo za dużo */ #define MAKS_CZCIONKI 30000 /* * main * */ int main (int argc, char *argv[]) { int nCzcionki; char **szaNazwyCzcionek; int i; /* --- Inicjacja GTK+. Potrzebna do wywołania GDK_DISPLAY --- */ gtk_init (&argc, &argv); /* --- Pobieramy nazwy czcionek --- */
Część III Rysowanie, kolor i GDK
312
szaNazwyCzcionek = XListFonts (GDK_DISPLAY (), "*", MAKS_CZCIONKI, &nCzcionki); /* --- Sprawdzamy uzyskane liczby --- */ if (nCzcionki == MAKS_CZCIONKI) { /* --- W systemie zainstalowano MNÓSTWO czcionek --- */ printf ("W systemie jest wiele czcionek. " "Nie można wyświetlić wszystkich."); } /* --- Wyświetlamy czcionki --- */ for (i = 0; i < nCzcionki; i++) { /* --- Pobieramy nazwę --- */ printf ("%s\n", szaNazwyCzcionek[i]); } XFreeFontNames (szaNazwyCzcionek); return (0); }
Możemy zmodyfikować przykład z oknem kolorów tak, aby użytkownik mógł wybierać czcionkę dla przycisku z okna wyboru czcionki. Przykład ten jest niemal identyczny, jak poprzedni, ale zamiast wybierać kolor dla przycisku, będziemy wybierać dla niego czcionkę. Okno dialogowe zwraca nazwę czcionki, którą można przekazać funkcji gtk_load_font w celu załadowania czcionki. Następnie można uaktualnić styl przycisku. Rysunek 11.3 przedstawia okno wyboru czcionki w działaniu. Zauważmy, że czcionka niekoniecznie musi być czcionką angielską. /* * Plik: czcionkaprz.c * */ #include #include GtkWidget *przycisk; GtkWidget *ypole; gchar *PobierzCzcionke (); /*
Style, kolory, czcionki, wskaźniki myszy i referencje
* NowyStyl * * Tworzy nowy styl na podstawie przekazanej czcionki */ GtkStyle *NowyStyl (GdkFont *c) { GtkStyle *styl; GtkStyle *styl_dom; /* --- Pobieramy styl domyślny --- */ styl_dom = gtk_widget_get_default_style (); /* --- Tworzymy jego kopię --- */ styl = gtk_style_copy (styl_dom); styl->font = c; /* --- Zwracamy styl --- */ return (styl); } /* * ZamknijOknoApl * * Zamyka aplikację */ gint ZamknijOknoApl (GtkWidget *kontrolka, gpointer gdane) { g_print ("Kończę pracę...\n"); gtk_main_quit (); return (FALSE); } /* * UstawStylRekurencyjnie * Ustawia styl kontrolki i wszystkich jej potomków */ void UstawStylRekurencyjnie (GtkWidget *kontrolka, gpointer dane) { GtkStyle *styl; /* --- pobieramy styl --- */ styl = (GtkStyle *) dane;
313
Część III Rysowanie, kolor i GDK
314 /* --- ustawiamy styl kontrolki --- */ gtk_widget_set_style (kontrolka, styl);
/* --- jeśli kontrolka może posiadać potomków --- */ if (GTK_IS_CONTAINER (kontrolka)) { /* --- ustawiamy także styl wszystkich potomków --- */ gtk_container_foreach (GTK_CONTAINER (kontrolka), UstawStylRekurencyjnie, styl); } } /* * KliknietoPrzycisk * * Procedura obsługi zdarzenia, wywoływana po kliknięciu przycisku. * Możemy jej użyć, ponieważ okno wyboru czcionki jest tutaj modalne, * i nie oddaje sterowania, dopóki nie zostanie pobrany czcionka */ void KliknietoPrzycisk (GtkWidget *kontrolka, gpointer gdane) { GtkStyle *styl; char *szCzcionka; GdkFont *czcionka; /* --- Wywołujemy okno dialogowe, aby pobrać czcionkę --- */ szCzcionka = PobierzCzcionke (); printf ("PobierzCzcionke=%s\n", szCzcionka); czcionka = gdk_font_load (szCzcionka); g_free (szCzcionka); /* --- Tworzymy styl na podstawie koloru --- */ styl = NowyStyl (czcionka); /* --- Ustawiamy nowy styl kontrolki */ UstawStylRekurencyjnie (kontrolka, (gpointer) styl); } /* * UtworzPrzycisk * * Tworzy przycisk i dodaje go do pionowego pola pakującego.
Style, kolory, czcionki, wskaźniki myszy i referencje
* Ustawia także procedurę obsługi zdarzenia na "KliknietoPrzycisk" */ void UtworzPrzycisk (GtkWidget *ypole, char *etykieta) { GtkWidget *przycisk; /* --- Tworzymy przycisk --- */ przycisk = gtk_button_new_with_label (etykieta); gtk_signal_connect (GTK_OBJECT (przycisk), "clicked", GTK_SIGNAL_FUNC (KliknietoPrzycisk), NULL); /* --- Umieszczamy przycisk w polu pakującym --- */ gtk_box_pack_start (GTK_BOX (ypole), przycisk, FALSE, FALSE, 0); /* --- Uwidaczniamy przycisk --- */ gtk_widget_show (przycisk); } /* * main * * Tutaj zaczyna się program */ int main (int argc, char *argv[]) { GtkWidget *okno; /* --- Inicjacja gtk, obsługa parametrów wiersza polecenia --- */ gtk_init (&argc, &argv); /* --- Tworzymy okno --- */ okno = gtk_window_new (GTK_WINDOW_TOPLEVEL); /* --- Musimy wiedzieć, kiedy okno będzie zamykane --- */ gtk_signal_connect (GTK_OBJECT (okno), "delete_event", GTK_SIGNAL_FUNC (ZamknijOknoApl), NULL); /* --- Trochę miejsca wokół obiektów w pojemniku --- */ gtk_container_border_width (GTK_CONTAINER (okno), 15); /* --- Tworzymy pionowe pole pakujące --- */ ypole = gtk_vbox_new (FALSE, 0);
315
Część III Rysowanie, kolor i GDK
316
/* --- Tworzymy przyciski --- */ UtworzPrzycisk (ypole, "Wybierz czcionkę"); UtworzPrzycisk (ypole, "Zażółć gęślą jaźń"); /* --- Teraz uwidaczniamy okno --- */ gtk_widget_show (ypole); gtk_container_add (GTK_CONTAINER (okno), ypole); gtk_widget_show (okno); /* --- Nie wracamy aż do zamknięcia aplikacji --- */ gtk_main (); /* --- Kod wyjściowy --- */ return 0; }
Poniżej znajduje się kod, tworzący okno wyboru czcionki. Po wywołaniu funkcji PobierzCzcionke zwraca nazwę czcionki w łańcuchu gchar *, który powinien zostać zwolniony, kiedy nie będzie już potrzebny. /* * Plik: wybczcionki.c * * Interfejs okna wyboru czcionki. Tworzy modalne okno dialogowe, * w którym można wybrać czcionkę. Funkcja PobierzCzcionke zwraca * nazwę czcionki. */ #include gchar *szNazwaCzcionki = NULL; /* * KliknietoOK * * Kliknięto przycisk OK */ void KliknietoOK (GtkWidget *kontrolka, GtkWidget *wybcz) { /* --- Rzutujemy na właściwy typ --- */ GtkFontSelectionDialog *owc = (wybcz); /* --- Pobieramy nazwę czcionki --- */
GTK_FONT_SELECTION_DIALOG
Style, kolory, czcionki, wskaźniki myszy i referencje
szNazwaCzcionki = gtk_font_selection_dialog_get_font_name (owc); /* --- Wyświetlamy nazwę czcionki --- */ printf ("Kliknięto OK - %s\n", szNazwaCzcionki); /* --- Usuwamy okno wyboru czcionki --- */ gtk_widget_destroy (wybcz); } /* * Zamknij * * Kończy program */ void Zamknij (GtkWidget *kontrolka, gpointer dane) { gtk_main_quit (); } /* * PobierzCzcionke * * Wyświetla okno wyboru czcionki i umożliwia użytkownikowi * wybranie czcionki. Zwraca łańcuch gchar * z nazwą czcionki. */ gchar *PobierzCzcionke () { GtkWidget *kontrolka; GtkFontSelectionDialog *wybcz; szNazwaCzcionki = NULL; /* --- Tworzymy okno wyboru czcionki --- */ kontrolka = gtk_font_selection_dialog_new("Okno wyboru czcionki"); /* --- Rzutujemy na właściwy typ --- */ wybcz = GTK_FONT_SELECTION_DIALOG (kontrolka); /* --- Funkcja zwrotna dla przycisku OK --- */ gtk_signal_connect (GTK_OBJECT (wybcz->ok_button), "clicked", GTK_SIGNAL_FUNC (KliknietoOK), wybcz); /* --- Funkcja zwrotna dla przycisku Cancel --- */ gtk_signal_connect_object (GTK_OBJECT (wybcz->cancel_button),
317
Część III Rysowanie, kolor i GDK
318
"clicked", GTK_SIGNAL_FUNC (gtk_widget_destroy), GTK_OBJECT (wybcz)); /* --- Sygna³ destroy --- */ gtk_signal_connect (GTK_OBJECT (wybcz), "destroy", GTK_SIGNAL_FUNC (Zamknij), wybcz); /* --- Pokazujemy okno dialogowe --- */ gtk_widget_show (kontrolka); /* --- Modalne - czekamy, aż zostanie usunięte --- */ gtk_main (); /* --- Zwracamy nazwę czcionki --- */ return (szNazwaCzcionki); }
Wskaźniki myszy Wskaźniki można wykorzystać do przekazania użytkownikom różnych informacji. Wskaźnik zmienia kształt na przykład wtedy, kiedy przesuniemy go nad pole wejściowe (przypomina dużą literę I) albo podczas zmiany rozmiaru okna (wskaźnik w kształcie strzałek). Czasem tego rodzaju wskazówki mają duże znaczenie. GTK+ zawiera spory zbiór wbudowanych wskaźników; pełną listę można zobaczyć w podkatalogu GDK katalogu instalacyjnego GTK+. Plik gdkcursors.h zawiera definicje wbudowanych wskaźników, które możemy wykorzystać we własnych programach, na przykład GDK_ARROW albo GDK_CLOCK. Wbudowany wskaźnik tworzymy przy pomocy funkcji gdk_cursor_new, przekazując jej identyfikator wskaźnika. Wskaźnik przypisuje się do okna, a nie do kontrolki, jednak łatwo jest ustalić okno, z którym związana jest kontrolka, sprawdzając pole window struktury kontrolki. /* --- tworzymy nowy wskaźnik --- */ wskaznik = gdk_cursor_new (GDK_CROSS); /* --- przypisujemy wskaźnik do przycisku --- */ gdk_window_set_cursor (przycisk->window, wskaznik);
Dosyć proste, ale GTK+ dysponuje ograniczoną liczbą wbudowanych wskaźników. A gdybyśmy zechcieli stworzyć własny wskaźnik? Można to zrobić przy pomocy danych xpm, tak jak w przypadku ikon dla przy-
Style, kolory, czcionki, wskaźniki myszy i referencje
319
cisków. Trik polega na tym, że rysunek musi zawierać trzy kolory: jeden dla pierwszego planu, jeden dla tła, a jeden dla koloru przezroczystego. Dane xpm należy przekształcić na dwie bitmapy GdkBitmap-jedną dla pierwszego planu, a drugą dla tła. Po przeprowadzeniu konwersji utworzenie wskaźnika nie przedstawia żadnych problemów. Rozważmy na przykład następujący rysunek xpm: static char *wskaznik_dlon[] = { "32 32 3 1", " c None", ". c #000000", "+ c #FFFFFF", " ", " ", " ... ", " .+++. ... ", " .+++. .+++. ... ", " ... .+++. .+++. .+++. ", " .+++. .+++. .+++. .+++. ", " .+++. .+++. .+++. .+++. ", " .+++. .+++. .+++. .+++. ", " .+++. .+++..+++. .+++. ", " .+++..+++..+++. .+++. ", " .+++.+++..+++..+++. ", " ... .++++++++++++++++. ", " .+++. .+++++++++++++++. ", " .++++..+++++++++++++++. ", " ..+++++++++++++++++++. ", " .++++++++++++++++++. ", " .+++++++++++++++++. ", " ..++++++++++++++. ", " .++++++++++++. ", " .++++++++++. ", " .......... ", " ", " ", " ", " ", " ", " ", " ", " ",
Część III Rysowanie, kolor i GDK
320 " "
", "};
Jest to trzykolorowy rysunek dłoni, o wysokości i szerokości 32 pikseli. Kolory zdefiniowane w rysunku są ignorowane. Na podstawie tych danych możemy uzyskać rysunek pierwszoplanowy i maskę, używając funkcji zapożyczonej z programu gnumeric (czy oprogramowanie o otwartym źródle nie jest wspaniałe?). Funkcja ta przyjmuje dane xpm i zwraca dwie bitmapy: jedną dla rysunku pierwszoplanowego, a drugą dla rysunku maski. void utworz_bitmape_i_maske_z_xpm (GdkBitmap **bitmapa, GdkBitmap **maska, gchar **xpm) { int wysokosc, szerokosc, kolory; char bufor_piksmapy [(32 * 32)/8]; char bufor_maski [(32 * 32)/8]; int x, y, piks; int kolor_przezrocz, kolor_czarny; sscanf (xpm [0], "%d %d %d %d", &wysokosc, &szerokosc, &kolory, &piks); g_assert (wysokosc == 32); g_assert (szerokosc == 32); g_assert (kolory == 3); kolor_przezrocz = ' '; kolor_czarny = '.'; for (y = 0; y < 32; y++){ for (x = 0; x < 32;){ char wartosc = 0, wartmaski = 0; for (piks = 0; piks < 8; piks++, x++){ if (xpm [4+y][x] != kolor_przezrocz){ wartmaski |= 1 window, wskaznik); } /* * --- main * * Tutaj zaczyna się program * * Zamiast powielać kod, wywołujemy funkcję UtworzTabele. * Wykona ona całą pracę - określamy tylko, jak powinno * wyglądać okno. */ int main (int argc, char *argv[]) { /* --- Inicjacja GTK --- */ gtk_init (&argc, &argv); /* --- Nie ustawiamy żadnych znaczników --- */ UtworzTabele ("Brak znaczników", 0, 0); /* --- Uruchamiamy pętlę GTK --- */ gtk_main (); exit (0); }
Referencje Każde działanie w GTK+ wiąże się z przydzielaniem elementów. Style są tworzone, albo kopiowane z już istniejących. Tworzone są kontrolki, a czcionki są ładowane i przypisywane do stylów. Powtarzanie tych procesów doprowadziłoby w końcu do wyczerpania się pamięci komputera gdyby nie istniał sposób łatwego zarządzania jej użyciem. GTK+ zarządza używaną przez siebie pamięcią przy pomocy referencji (odwołań) do obiektów. Referencja do obiektu zapobiega jego usunięciu. Za każdym razem, kiedy jakiś obiekt (na przykład czcionka) jest przypisywany do innego obiektu przez procedury GTK+, jego licznik referencji jest zwiększany o 1. Kiedy
328
Część III Rysowanie, kolor i GDK
jakiś obiekt zostanie usunięty, GTK+ sprawdza wszystkie obiekty, do których się odwoływał i zmniejsza ich liczniki referencji. Jeśli licznik referencji jest równy zero, GTK+ zakłada, że obiekt jest już niepotrzebny i zwalnia go. Większość tych działań odbywa się w sposób przezroczysty i zazwyczaj nie stanowi problemu w przypadku niewielkich aplikacji. Jeśli jednak będziemy nieostrożni, w większych programach możemy mieć kłopoty z „wyciekaniem” pamięci. Kiedy tworzone są kontrolki, ich licznik referencji początkowo wynosi zero. Jeśli zostaną w ten czy inny sposób przypisane do pojemnika, licznik referencji jest zwiększany o 1, aby zaznaczyć, że pojemnik odwołuje się do kontrolki. Kiedy pojemnik jest usuwany, zmniejsza on licznik referencji wszystkich kontrolek, do których się odwołuje - czyli wszystkich obiektów w pojemniku. Jeśli dla którejkolwiek kontrolki licznik referencji spadnie do zera, kontrolka zostanie usunięta. Ma to sens, ponieważ w tym momencie kontrolka nie powinna już być używana. Można zapobiec usunięciu kontrolki, zwiększając jej licznik referencji; w takim przypadku po usunięciu pojemnika kontrolka nadal będzie istnieć. Oczywiście, nie będzie z niej żadnego pożytku, o ile nie zachowamy wskaźnika do kontrolki, aby użyć go w innym miejscu programu. Obiekty nie będące kontrolkami są traktowane nieco inaczej. Po utworzeniu ich licznik referencji jest automatycznie ustawiany na jeden. Obiekty takie zazwyczaj reprezentują style, piksmapy lub inne wielokrotnie używane elementy, które chcemy przydzielić raz, a wykorzystać w programie kilka razy. Nie chcemy, aby aplikacja usunęła stworzony przez nas styl tylko dlatego, że usunęliśmy ostatnią kontrolkę, która go używała. Jednak często musimy usunąć referencję do stworzonych przez nas elementów jednorazowego użytku, w następujący sposób: /* --- tworzymy piksmapę na podstawie danych --- */ piksmapa = gdk_pixmap_create_from_xpm_d ( glowne_okno->window, &maska, NULL, (gchar **) dane_xpm); /* --- tworzymy kontrolkę piksmapy --- */ kontrolka = gtk_pixmap_new (piksmapa, NULL); /* --- nie potrzebujemy już piksmapy --- */ gdk_pixmap_unref (piksmapa);
Usunięcie referencji do piksmapy zazwyczaj spowodowałoby usunięcie piksmapy, ale tutaj, w rezultacie działania funkcji gtk_pixmap_new, do piksmapy odwołuje się kontrolka. Licznik referencji piksmapy zostanie
Style, kolory, czcionki, wskaźniki myszy i referencje
329
zmniejszony, kiedy usuniemy kontrolkę, a jeśli spadnie do zera, piksmapa również zostanie usunięta.
Podsumowanie Style składają się z kolorów i czcionek i określają wygląd kontrolek. Style można modyfikować indywidualnie, albo można ustalić styl domyślny dla nowo tworzonych kontrolek. GTK+ posiada wbudowany zbiór wskaźników myszy, dzięki którym można modyfikować wygląd wskaźnika myszy w obrębie kontrolki. Można używać wbudowanych wskaźników, ale można także łatwo stworzyć własne. GTK+ używa referencji, które pomagają w zarządzaniu obiektami i zwalnia obiekty, których licznik referencji spadnie do zera.
Rozdział 12 Przeglądarka cząsteczek Wyświetlanie informacji przy użyciu trójwymiarowej grafiki jest zawsze interesującym zagadnieniem. W rozdziale wykorzystamy GDK (Graphics Drawing Kit) do napisania przeglądarki plików .pdb, która pozwoli oglądać cząsteczki na kontrolce obszaru rysunkowego (patrz rysunek 12.1). W programie wykorzystamy arytmetykę trójwymiarową; jeśli czytelnicy nie czują się pewnie w tym temacie, mogą po prostu zapoznać się z ogólnymi koncepcjami, ignorując mnożenie macierzy. Jeśli zaś chcieliby dowiedzieć się więcej o obliczeniach trójwymiarowych, mogą sięgnąć po jedną z wielu książek traktujących o grafice komputerowej.
Rysunek 12.1. Przeglądarka cząsteczek.
Oprócz samego wyświetlania cząsteczki, aplikacja będzie dysponowała możliwością jej obracania. Użytkownik będzie także mógł wyświetlać cząsteczkę w kolorze i z tekstem, opisującym poszczególne atomy. Aby
Część III Rysowanie, kolor i GDK
332
obracanie cząsteczki nie powodowało zakłóceń na ekranie, wykorzystamy podwójne buforowanie.
Format pliku Aby wyświetlać pliki .pdb, musimy umieć je odczytać, a także zrozumieć format opisu cząsteczek. Format pliku .pdb jest dość skomplikowany, ponieważ zaprojektowano go do opisu złożonych cząsteczek. Na szczęście możemy zignorować większość danych, przechowywanych w pliku .pdb. Interesują nas tylko nazwy atomów, ich pozycje, oraz wiązania pomiędzy nimi. Pliki .pdb posiadają dwie części: pierwsza opisuje atomy, a druga (opcjonalna) wiązania pomiędzy atomami. Pełen opis atomów jest dość skomplikowany: Format rekordu KOLUMN Y
TYP DANYCH
POLE
OPIS
1 -6
Nazwa rekordu
"ATOM "
07 - 11
Liczba całkowita
serial
Numer kolejny atomu Nazwa atomu
13 -16
Atom
name
17
Znak
altLoc
Wskaźnik alternatywnego położenia
18 - 20
Nazwa pozostałości
resName
Nazwa pozostałości
22
Znak
chainID
Identyfikator łańcucha
23 - 26
Liczba całkowita
resSeq
Numer kolejny pozostałości
27
Znak
iCode
Kod wstawiania pozostałości
31 - 38
L. rzeczywista (8.3)
x
Ortogonalna współrzędna X w angstremach.
39 - 46
L. rzeczywista (8.3)
y
Ortogonalna współrzędna Y w angstremach.
47 - 54
L. rzeczywista (8.3)
z
Ortogonalna współrzędna Z w angstremach.
55 - 60
L. rzeczywista (6.3)
occupanc Zajętość y
61 - 66
L. rzeczywista (6.3)
tempFact or
Czynnik temperaturowy
73 - 76
LString(4)
segID
Identyfikator segmentu,
77 - 78
LString(2)
element
Symbol pierwiastka, wyrównany do prawej
79 - 80
LString(2)
charge
Ładunek atomu
wyrównany do lewej
Przeglądarka cząsteczek
333
Najważniejszymi informacjami są nazwa rekordu, „Atom” i współrzędne przestrzenne (x, y, z). Druga część pliku .pdb jest opcjonalna i zawiera informacje, potrzebne do wyświetlenia wiązań atomowych. Linie CONECT opisują atom i wszystkie jego wiązania z innymi atomami. Pierwsza liczba po słowie CONECT jest numerem kolejnym atomu w cząsteczce, a pozostałe są numerami kolejnymi atomów, z którymi jest związany. Przykładowy plik .pdb z danymi o wiązaniach wygląda następująco: ATOM ATOM ATOM ATOM ATOM ATOM ATOM ATOM ATOM ATOM ATOM ATOM ATOM ATOM ATOM ATOM ATOM ATOM TER CONECT CONECT CONECT CONECT CONECT CONECT CONECT CONECT CONECT CONECT CONECT CONECT CONECT
1 O11 2 O12 3 O13 4 N1 5 C1 6 C2 7 C3 8 C3' 9 C11 10 C12 11 C13 12 B1 13 N1* 14 C3* 15 O11* 16 C1* 17 C3'* 18 C11*
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
1 2 3 4 5 6 7 8 9 10 11 12 13
12 12 12 6
7
13 8
14
2 14
3 16
9 10 11 5 4 4 4 7 1 2 3 1 6
2.227 4.387 3.470 1.032 1.135 1.192 0.762 0.507 2.759 4.372 2.451 3.078 1.032 0.762 2.227 1.135 0.507 2.759
3.257 2.116 2.116 3.192 4.580 2.116 2.796 3.783 4.526 2.116 2.116 2.116 1.041 1.437 0.976 -0.347 0.450 -0.293
15
9.904 10.202 8.123 13.498 13.046 12.739 14.797 15.877 9.626 11.603 7.171 9.530 13.498 14.797 9.904 13.046 15.877 9.626
Część III Rysowanie, kolor i GDK
334 CONECT CONECT CONECT CONECT
14 15 16 17
7 12 13 14
13
17
Przyjrzyjmy się pierwszej linii pliku. Zaczyna się od słowa ATOM, co oznacza, że linia opisuje atom. Następny znak, 1, jest indeksem (numerem kolejnym) atomu. Atomy powinny być uporządkowane. O11 jest nazwą atomu - w tym przypadku O oznacza prawdopodobnie tlen. Położenie atomu określają trzy ostatnie liczby w linii. Są to współrzędne x, y i z atomu w cząsteczce. ATOM
1 O11
1
2.227 3.257 9.904
Opis atomów kończy się na linii o etykiecie TER, po której następują wiązania. Poszczególne linie zaczynają się od słowa CONECT, i zawierają numer atomu oraz numery wszystkich atomów, z którymi łączą go wiązania. Pierwsza linia opisu wiązań wygląda następująco: CONECT
1
9 12
Oznacza ona, że atom 1 ma wiązania z atomami 9 i 12. Atomy niekoniecznie muszą mieć wiązania, ale zazwyczaj je mają.
Struktury danych Struktura danych atomu (typAtom) przechowuje nazwę atomu, dwa zbiory współrzędnych oraz listę atomów, z którymi atom jest połączony wiązaniami. Struktura posiada dwa zbiory współrzędnych, ponieważ przechowujemy w niej współrzędne pierwotne oraz współrzędne przekształcone. Potrzebujemy przekształconych współrzędnych dlatego, że cząsteczka będzie obracana wokół osi-a w miarę obrotu będzie zmieniać się położenie atomów w trójwymiarowej przestrzeni. Określają je przekształcone współrzędne, obliczane od nowa po każdym przesunięciu cząsteczki. Przekształcone współrzędne odzwierciedlają miejsce, w którym atom zostanie narysowany. Ponieważ każdy atom może mieć wiązania z jednym lub wieloma atomami, struktura typAtom przechowuje wszystkie wiązania w łączonej liście GSList. typedef struct { char *szNazwa; double x; double y; double z; double tx;
// --- Nazwa atomu // --- Pierwotne współrzędne // // // --- Współrzędne po translacji
Przeglądarka cząsteczek
double ty; // double tz; // GSList *listaWiazan; } typAtom;
335 // --- Atomy, z którymi atom ma wiązania
Wiązania opisują związki, które umożliwiają połączenie atomów liniami. Dodatkowy znacznik przyspiesza rysowanie-wiązanie powinno być rysowane tylko raz dla każdej pary atomów. typedef struct { typAtom *atom1; /* --- Pierwszy atom we wiązaniu --- */ typAtom *atom2; /* --- Drugi atom we wiązaniu--- */ int bNarysowane; /* --- narysowane--- */ } typWiazanie;
Atomy i wiązania są przechowywane w statycznych tablicach (autor nad tym ubolewa, ale dynamiczne przydzielanie pamięci oznaczałoby znacznie więcej pracy). Tablice ułatwiają rysowanie atomów.
Rysowanie w trzech wymiarach Ponieważ cząsteczka ma strukturę trójwymiarową, powinniśmy narysować ją tak, aby stworzyć iluzję trójwymiarowości. Podczas wyświetlania cząsteczek należy unikać oczywistych błędów, jak na przykład rysowania atomów bez uwzględnienia ich położenia w osi z. Spowodowałoby to, że atomy znajdujące się dalej znalazłyby na ekranie przed atomami położonymi bliżej oglądającego. Można uporać się z tym problemem, sortując atomy według przekształconej współrzędnej z. Jeśli narysujemy najpierw odległe atomy, mamy gwarancję, że atomy znajdujące się najbliżej oglądającego pozostaną na szczycie, i nie zostaną przykryte przez inne. Algorytm rysowania wyświetla najpierw najdalsze atomy, a potem przykrywa je bliższymi.
Kod źródłowy Cały kod służący do rysowania cząsteczek (z wyjątkiem procedur matematycznych operujących na macierzach) znajduje się w pliku czasteczka.c. Umieszczono tutaj procedury umożliwiające wczytywanie plików, rysowanie cząsteczek i wykonywanie innych operacji na cząsteczkach; tego rodzaju modularyzacja kodu pozwala na łatwe wykorzystanie go w innej aplikacji. Ponieważ większość nowych funkcji znajduje się w pliku
Część III Rysowanie, kolor i GDK
336
czasteczka.c, przejrzymy teraz jego zawartość i omówimy czynności wykonywane przez poszczególne procedury. WczytajCzasteczke
Główna procedura wczytująca plik musi zanalizować dwie jego części. Najpierw wczytuje atomy, pobierając z pliku ich nazwy oraz współrzędne. Następnie wczytuje wiązania, umieszczając je zarazem w strukturze danych atomu jak i w tablicy wiązań. Funkcja ta jest wywoływana podczas uruchamiania aplikacji, i wczytuje domyślny plik (czasteczka.pdb). Można ją także wywołać wybierając opcję menu Plik/Nowy, co umożliwia wczytanie innych plików .pdb. Kiedy wczytywany jest nowy plik, trzeba wyzerować niektóre dane (na przykład macierze). Procedura Wczytaj Czasteczke wymusza także przerysowanie ekranu po wczytaniu nowych danych. /* * WczytajCzasteczke * * Wczytuje cząsteczkę z pliku .pdb o podanej nazwie. * Zachowuje informacje w zdefiniowanych strukturach * danych. */ void WczytajCzasteczke (char *sNazwaPliku) { FILE *fp; char bufor[120]; float x, y, z; int nIndeks1; int nIndeks2; char szNazwa[120]; char *sTmcz; typAtom *atom; char szTmcz[20]; Inicjuj3d (); nAtomy = 0; nWiazania = 0; /* --- Przed wczytywaniem pliku zerujemy macierze --- */ if (macierz) { jednostka (macierz); jednostka (amacierz); jednostka (tmacierz);
Przeglądarka cząsteczek
} nPromienCzasteczki = 2; /* --- Otwieramy plik do odczytu --- */ fp = fopen (sNazwaPliku, "r"); /* --- Wczytujemy linię z pliku --- */ while (fgets (bufor, sizeof (bufor), fp)) { /* --- Jeśli jest to atom --- */ if (strncmp (bufor, "ATOM", 4) == 0) { /* --- Wczytujemy dane atomu, znając * strukturę pliku .pdb */ strncpy (szNazwa, &bufor[12], 4); szNazwa[4] = 0; strncpy (szTmcz, &bufor[30], 8); szTmcz[8] = 0; x = atof (szTmcz); strncpy (szTmcz, &bufor[38], 8); szTmcz[8] = 0; y = atof (szTmcz); strncpy (szTmcz, &bufor[46], 8); szTmcz[8] = 0; z = atof (szTmcz); /* --- Indeksy atomów zaczynają się od 1 --- */ nAtomy++; /* --- Wypełniamy strukturę danych --- */ atom = &listaatomow[nAtomy]; atom->x = x; atom->y = y; atom->z = z; atom->szNazwa = strdup (szNazwa); atom->listaWiazan = NULL; sortindeks[nAtomy-1] = nAtomy; /* --- Czy linia opisuje wiązanie? --- */ } else if (strncmp (bufor, "CONECT", 6) == 0) {
337
Część III Rysowanie, kolor i GDK
338
/* --- Pobieramy pierwszy atom wiązania --- */ sTmcz = PobierzNastepnaWartosc (&bufor[6], &nIndeks1); /* --- Pobieramy następne atomy wiązania --- */ while (sTmcz = PobierzNastepnaWartosc (sTmcz, &nIndeks2)) { /* --- Wiązanie jest od nIndeks1 do nIndeks2 --- */ listawiazan[nWiazania].atom1 = &listaatomow[nIndeks1]; listawiazan[nWiazania].atom2 = &listaatomow[nIndeks2]; /* --- Oczywiście atom musi wiedzieć, * jakie tworzy wiązania... */ listaatomow[nIndeks1].listaWiazan = g_slist_append(listaatomow[nIndeks1].listaWiazan, &listawiazan[nWiazania]); /* --- ...i drugi atom również --- */ listaatomow[nIndeks2].listaWiazan = g_slist_append (listaatomow[nIndeks2].listaWiazan, &listawiazan[nWiazania]); /* --- Zwiększamy liczbę wiązań --- */ nWiazania++; } } } /* --- Znajdujemy prostopadłościan ograniczający --- */ ZnajdzPO (); /* --- Sortujemy atomy --- */ SortujAtomy (listaatomow, sortindeks); OdswiezCzasteczke (); }
ZnajdzPO Prostopadłościan ograniczający jest to najmniejszy prostopadłościan, który mógłby pomieścić wyświetlaną cząsteczkę. Procedura ZnajdzPO oblicza wymiary prostopadłościanu ograniczającego i pozwala przeskalować obiekt tak, aby zmieścił się na ekranie.
Przeglądarka cząsteczek
/* * ZnajdzPO * * Znajduje najmniejszy prostopadłościan, który mógłby * pomieścić wszystkie atomy w cz±steczce. */ void ZnajdzPO () { int i; typAtom *atom; /* --- Najpierw prostopadłościan zawiera pojedynczy atom. Atomy zaczynają się od 1 --- */ atom = &listaatomow[1]; xmin = atom->x; xmax = atom->x; ymin = atom->y; ymax = atom->y; zmin = atom->z; zmax = atom->z; /* --- Teraz dodajemy całą resztę --- */ for (i = 2; i x < xmin) xmin = atom->x; if (atom->x > xmax) xmax = atom->x; if (atom->y < ymin) ymin = atom->y; if (atom->y > ymax) ymax = atom->y; if (atom->z < zmin) zmin = atom->z; if (atom->z > zmax) zmax = atom->z; } /* --- ...i mamy nasz prostopadłościan --- */ }
339
340
Część III Rysowanie, kolor i GDK
Sortowanie atomów Aby uzyskać poprawny, trójwymiarowy obraz, trzeba narysować cząsteczkę zaczynając od najdalszego atomu, a kończąc na najbliższym. Musimy więc posortować atomy według ich przekształconej współrzędnej z. Nie możemy wykorzystać rzeczywistej współrzędnej z, ponieważ użytkownik mógł obrócić cząsteczkę, i ogląda ją w takiej właśnie obróconej postaci. Po przeprowadzeniu sortowania najbliższe atomy zostaną narysowane na wierzchu, tworząc poprawny (choć iluzoryczny) trójwymiarowy obraz. Zamiast sortować same atomy, sortujemy ich indeksy, ponieważ przesuwanie liczb całkowitych w obrębie tablicy jest dużo szybsze, niż przemieszczanie całych struktur. /* * SortujAtomy * * Wywołuje funkcje sortującą */ void SortujAtomy (typAtom *listaatomow, int *sortindeks) { QSortujAtomy (listaatomow, sortindeks, 0, nAtomy-1); } /* * QSortujAtomy * * Sortowanie typu quicksort wszystkich atomów w cząsteczce. * * Uwaga: Zamiast sortować samą listę atomów, sortujemy * tablicę indeksów (sortindeks). Operowanie na liczbach * całkowitych jest szybsze, niż przemieszczanie struktur * zwłaszcza, że musimy robić to w czasie rzeczywistym i * bardzo często. * * listaatomow - lista atomów do posortowania * sortlist - tablica indeksów */ void QSortujAtomy (typAtom *listaatomow, int *sortindeks, int dol0, int gora0) { int nTmcz; int dol = dol0; int gora = gora0;
Przeglądarka cząsteczek
int
341
srodek;
if (gora0 > dol0) { srodek = listaatomow[sortindeks[(dol0 + gora0) / 2]].tz; while (dol dol0 && listaatomow[sortindeks[gora]].tz > srodek) { gora--; } if (dol xw) xw = zw; f1 = nSzerEkranu / xw; f2 = nWysokEkranu / xw; xczyn = .7 * (f1 < f2 ? f1 : f2); /* --- Najpierw czynimy macierz macierzą jednostkową --- */ jednostka (macierz); /* --- Przestawiamy macierz wokół środka translacji. * Dzięki temu cząsteczka będzie wyśrodkowana wokół osi, * biegnących przez środek prostopadłościanu * ograniczającego cząsteczkę. */ przestaw (macierz, -(xmin + xmax) / 2, -(ymin + ymax) / 2, -(zmin + zmax) / 2); /* --- Obracamy obraz wokół osi. amacierz określa, w jakim stopniu należy obrócić cząsteczkę */ mnoz (macierz, amacierz); /* --- Skalujemy cząsteczkę na podstawie szerokości ekranu --- */ skaluj3 (macierz, xczyn, -xczyn, 16 * xczyn / nSzerEkranu); /* --- Przesuwamy cząsteczkę na podstawie szerokości i wysokości ekranu --- */ przestaw (macierz, nSzerEkranu / 2, nWysokEkranu / 2, 10); /* --- Obliczamy nowe położenia wszystkich punktów --- */ TransformujPunkty (macierz); /* --- Rysujemy cząsteczkę na podstawie przekształconych współrzędnych --- */ RysujCzasteczke (g); }
343
Część III Rysowanie, kolor i GDK
344 Rysowanie wiązań
Można narysować wiązania podczas rysowania każdego atomu, ponieważ wiemy, które wiązanie należy do którego atomu. Metoda taka spowodowałaby jednak, że każde wiązanie byłoby rysowane dwa razy, co jest niepożądane. Można także rysować wiązania albo przed, albo po narysowaniu atomów, ale w obu przypadkach zrujnowalibyśmy iluzję trójwymiarowości, którą usiłujemy nadać wyświetlanej cząsteczce. Najlepszym rozwiązaniem jest narysowanie wiązania tylko podczas rysowania odleglejszego atomu. Dzięki temu wiązanie zostanie narysowane tylko raz i nie będzie przykrywać atomów znajdujących się bliżej oglądającego. Cząsteczka jest rysowana w funkcji RysujCzasteczke. Atomy są rysowane w kolejności określonej przez tablicę sortindeks, która przechowuje indeksy posortowanych atomów. Dzięki użyciu tej tablicy kolejność rysowania atomów odpowiada ich odległości od oglądającego. W znaczniku bNarysowane zapamiętujemy, czy wiązanie zostało już narysowane; jeśli tak, nie będzie rysowane ponownie (nie ma sensu tracić czasu procesora). Wiązania są rysowane wraz z odpowiednimi atomami, aby zapewnić dobry trójwymiarowy efekt. /* * RysujCzasteczke * * Rysuje cząsteczkę */ void RysujCzasteczke (typGrafika *g) { int nIndeks; int nSrednica; typWiazanie *wiazanie; GSList *lista; typAtom *atom; int i; GdkGC *pioro; /* --- Upewniamy się, że wszystko jest gotowe --- */ Inicjuj3d (); /* --- Sortujemy atomy --- */ SortujAtomy (listaatomow, sortindeks); /* --- Pobieramy średnicę cząsteczki --- */ nSrednica = nPromienCzasteczki + nPromienCzasteczki;
Przeglądarka cząsteczek
/* --- Jeśli pokazujemy wiązania... --- */ if (PokazLinie()) { /* --- Czyścimy znacznik, który wskazuje, że * to wiązanie zostało już narysowane */ for (i = 0; i < nWiazania; i++) { listawiazan[i].bNarysowane = FALSE; } } /* --- Wyświetlamy wszystkie atomy na liście --- */ for (i = 0; i < nAtomy; i++) { /* --- Używamy listy posortowanej - rysujemy od * najdalszego do najbliższego atomu. */ nIndeks = sortindeks[i]; /* --- Pobieramy atom, wskazywany przez indeks --- */ atom = &listaatomow[nIndeks]; /* --- Czy ten atom ma swój kolor? --- */ pioro = PobierzKolorAtomu (atom); /* --- Rysujemy koło w kolorze atomu --- */ gdk_draw_arc (g->piksmapa, pioro, TRUE, atom->tx - 3, atom->ty - 3, 7, 7, 0, 360 * 64); /* --- Jeśli włączono pokazywanie nazw... --- */ if (PokazEtykiety ()) { /* --- Wyświetlamy nazwę atomu --- */ if (atom->szNazwa) { gdk_draw_string (g->piksmapa, czcionka, pioro, atom->tx + 5, atom->ty, atom->szNazwa); } } /* --- Jeśli włączono pokazywanie wiązań... --- */ if (PokazLinie()) {
345
Część III Rysowanie, kolor i GDK
346
/* --- Rysujemy wszystkie wiązania atomu --- */ for (lista = atom->listaWiazan; lista; lista = lista->next) { /* --- Pobieramy wiązanie z listy --- */ wiazanie = (typWiazanie *) lista->data; /* --- Jeśli jeszcze nie było rysowane... --- */ if (wiazanie->bNarysowane == FALSE) { /* --- Rysujemy wiązanie (linię) --- */ RysujWiazanie (g, wiazanie->atom1, wiazanie->atom2); /* --- Zaznaczamy wiązanie jako narysowane --- */ wiazanie->bNarysowane = TRUE; } } } } }
Kolory atomów Atomy są wyświetlane w kolorach, jeśli wciśnięty jest odpowiedni przycisk na pasku narzędziowym. Jeśli przycisk nie jest wciśnięty, wszystkie atomy są rysowane na czarno. W przeciwnym przypadku atomy są rysowane w kolorze określonym na podstawie nazwy atomu w pliku. Jeśli na przykład nazwa atomu zaczyna się od N, jest to prawdopodobnie atom azotu i zostanie narysowany na niebiesko (procedura zwróci kontekst GdkGC z niebieskim kolorem). /* * PobierzKolorAtomu * * Zwraca kolor atomu na podstawie jego nazwy. * * Cząsteczki są rysowane albo na czarno, albo w * kolorze, zależnie od ustawienia przycisku na * pasku narzędziowym. * * Jeśli przycisk jest włączony...
Przeglądarka cząsteczek
* Jeśli nazwa zaczyna się od C (carbonium, węgiel), * rysujemy atom na czarno. Jeśli zaczyna się od * N (nitrogenium, azot), rysujemy go na niebiesko. * Jeśli zaczyna się od O (oxygenium, tlen), rysujemy * go na czerwono itd. Wszystkie inne atomy są * rysowane w ciemnoszarym kolorze. * * atom - atom, którego kolor należy określić */ GdkGC *PobierzKolorAtomu (typAtom *atom) { char szNazwa[10]; /* --- Nie ma nazwy --- */ if (atom->szNazwa == NULL) { return (pioroCzarne); } /* --- Nie pokazujemy kolorów --- */ if (!PokazRGB ()) { return (pioroCzarne); } PobierzNazweAtomu (szNazwa, atom); if (!strcmp (szNazwa, "CL")) { return (pioroZielone); } else if (!strcmp (szNazwa, "C")) { return (pioroCzarne); } else if (!strcmp (szNazwa, "S")) { return (pioroZolte); } else if (!strcmp (szNazwa, "P")) { return (pioroPomaranczowe); } else if (!strcmp (szNazwa, "N")) { return (pioroNiebieskie); } else if (!strcmp (szNazwa, "O")) { return (pioroCzerwone); } else if (!strcmp (szNazwa, "H")) { return (pioroBiale); } else { return (pioroCiemnoszare); }
347
Część III Rysowanie, kolor i GDK
348 }
configure_event Funkcja configure_event inicjuje struktury. Ponieważ wywoływana jest w razie tworzenia albo zmiany rozmiarów kontrolki obszaru rysunkowego, ustawia ona rozmiar drugoplanowej piksmapy na rozmiar kontrolki. /* * Tworzymy nową, drugoplanową piksmapę o właściwych rozmiarach */ static gint configure_event (GtkWidget *kontrolka, GdkEventConfigure *zdarzenie) { /* --- Struktura nie istnieje? --- */ if (g == NULL) { /* --- Tworzymy ją --- */ g = NowaGrafika (); } /* --- Piksmapa istnieje? --- */ if (g->piksmapa) { /* --- Zwalniamy ją --- */ gdk_pixmap_unref (g->piksmapa); } /* --- Tworzymy piksmapę --- */ g->piksmapa = gdk_pixmap_new (kontrolka->window, kontrolka->allocation.width, kontrolka->allocation.height, -1); /* --- Pobieramy szerokość i wysokość, żeby wyczyścić ekran --- */ nSzerEkranu = kontrolka->allocation.width; nWysokEkranu = kontrolka->allocation.height; /* --- Czyścimy ekran --- */ gdk_draw_rectangle (g->piksmapa, kontrolka->style->white_gc, TRUE, 0, 0,
Przeglądarka cząsteczek
349
kontrolka->allocation.width, kontrolka->allocation.height); /* --- Przerysowujemy cząsteczkę --- */ OdswiezCzasteczke (); return TRUE; }
expose_event Zdarzenie expose_event zachodzi wtedy, kiedy obszar ekranu wymaga uaktualnienia. W przypadku naszej aplikacji funkcja expose_event po prostu kopiuje drugoplanową piksmapę do kontrolki obszaru rysunkowego. Dlatego jest bardzo szybka. /* * expose_event * * Wywoływana wtedy, kiedy obszar ekranu został odsłonięty * i musimy go przerysować. Obszar jest kopiowany z * drugoplanowego bufora. */ static gint expose_event (GtkWidget *kontrolka, GdkEventExpose *zdarzenie) { gdk_draw_pixmap(kontrolka->window, kontrolka->style->fg_gc[GTK_WIDGET_STATE (kontrolka)], g->piksmapa, zdarzenie->area.x -1, zdarzenie->area.y -1 , zdarzenie->area.x -1, zdarzenie->area.y -1 , zdarzenie->area.width -1, zdarzenie->area.height - 1); return FALSE; }
Odśwież Cząsteczkę Funkcja ta wywoływana jest wtedy, kiedy z jakichś powodów należy przerysować cząsteczkę. Czyścimy drugoplanową piksmapę, rysujemy
Część III Rysowanie, kolor i GDK
350
na niej cząsteczkę, a następnie wywołujemy funkcję expose_event, która kopiuje drugoplanową piksmapę na ekran. /* * OdswiezCzasteczke * * Wywoływana wtedy, kiedy użytkownik przesuwa cząsteczkę * przy pomocy myszy. Powoduje to przerysowanie * cząsteczki. */ void OdswiezCzasteczke () { GdkRectangle uakt_prostokat; Inicjuj3d (); /* --- czyścimy piksmapę --- */ gdk_draw_rectangle (g->piksmapa, pioroSzare, TRUE, 0, 0, obszar_rys->allocation.width, obszar_rys->allocation.height); /* --- Rysujemy cząsteczkę na drugim planie --- */ rysuj (g); /* --- Odświeżamy cały ekran --- */ uakt_prostokat.x = 0; uakt_prostokat.y = 0; uakt_prostokat.width = obszar_rys->allocation.width; uakt_prostokat.height = obszar_rys->allocation.height; /* --- Wywołujemy funkcję expose_event, która * kopiuje drugoplanowy bufor do kontrolki */ gtk_widget_draw (obszar_rys, &uakt_prostokat); }
Cząsteczka jest rysowana na drugoplanowej piksmapie za każdym razem, kiedy użytkownik przesunie ją przy pomocy myszy. Istnieje niewielka różnica pomiędzy techniką używaną tutaj, a tą z aplikacji zegara (patrz rozdział 10, „GDK”): tam przerysowywanie zegara odbywało się
Przeglądarka cząsteczek
351
co sekundę i było powodowane przez czasomierz; tutaj cząsteczka jest rysowana na drugoplanowej piksmapie wtedy, kiedy użytkownik przeciągnie ją myszą.
Tworzenie obszaru rysunkowego Funkcja UtworzObszarRysunkowy jest wywoływana z pliku interfejs.c, który tworzy główne okno aplikacji. Pojedynczy blok kodu obejmuje wszystkie funkcje, które są niezbędne do stworzenia obszaru, na którym będą wyświetlane cząsteczki i nie ma żadnego wpływu na resztę aplikacji. Można wstawić tę funkcję do dowolnego programu bez żadnych zmian. Obszar rysunkowy odbiera sygnały expose_event, aby można było przerysować kontrolkę, oraz sygnały configure_event, dzięki czemu obszar rysunkowy będzie odpowiednio skonfigurowany i gotowy do rysowania. Musimy także sprawdzać sygnał motion_notify_event, aby wiedzieć, że użytkownik przeciąga cząsteczkę przy pomocy myszy. Nie możemy jednak bezpośrednio wykorzystać sygnału motion_notify_event w funkcji gtk_signal_ connect. W GTK+ niskopoziomowe zdarzenia GDK nie są wysyłane do aplikacji, o ile jawnie tego nie zażądamy. Posłużymy się funkcją gtk_widget_set_events, której można przekazać jedną z poniższych wartości: GDK_EXPOSURE_MASK GDK_POINTER_MOTION_MASK GDK_POINTER_MOTION_HINT_MASK GDK_BUTTON_MOTION_MASK GDK_BUTTON1_MOTION_MASK GDK_BUTTON2_MOTION_MASK GDK_BUTTON3_MOTION_MASK GDK_BUTTON_PRESS_MASK GDK_BUTTON_RELEASE_MASK GDK_KEY_PRESS_MASK GDK_KEY_RELEASE_MASK GDK_ENTER_NOTIFY_MASK GDK_LEAVE_NOTIFY_MASK GDK_FOCUS_CHANGE_MASK GDK_STRUCTURE_MASK
352
Część III Rysowanie, kolor i GDK
GDK_PROPERTY_CHANGE_MASK GDK_PROXIMITY_IN_MASK GDK_PROXIMITY_OUT_MASK W naszej aplikacji będziemy potrzebować GDK_POINTER_MOTION_MASK, który informuje, że przesunął się wskaźnik myszy. Istnieje jednak pewien problem: zdarzenia związane z ruchem myszy występują bardzo często, kiedy mysz jest przesuwana. Jeśli komputer nie jest wystarczająco szybki, aby na nie odpowiedzieć, albo przetwarzanie zajmuje zbyt wiele czasu, wówczas zdarzenia zaczynają się gromadzić. Sytuacja taka mogłaby mieć miejsce, kiedy wyświetlalibyśmy bardzo dużą cząsteczkę, wymagającą złożonych obliczeń i długiego rysowania. W takim przypadku przetwarzanie zdarzeń i rysowanie cząsteczki trwałoby nadal już po zaprzestaniu ruchu myszy (aby oczyścić kolejkę zdarzeń), co nie wygląda najlepiej (aby zapoznać się z tym problemem, możemy wykomentować GDK_POINTER_MOTION_HINT_MASK i przesunąć jedną ze złożonych cząsteczek, wyświetlaną wraz z kolorami, wiązaniami i nazwami atomów. Przesuwajmy ją przez kilka sekund i zatrzymajmy mysz). Lepiej jest skorzystać ze wskazówek o ruchu myszy. Zazwyczaj każdy ruch myszy generuje zdarzenie. Mogą one się nawarstwić i spowodować ociężałość aplikacji, która będzie próbowała oczyścić kolejkę z wszystkich nagromadzonych zdarzeń. Lepiej jest skorzystać z GDK_POINTER_MOTION_HINT_MASK i pozwolić aplikacji na przetwarzanie zdarzeń związanych z ruchem myszy w jej własnym tempie. Zdarzenia nie nawarstwiają się w trakcie ruchu myszy; generowane jest pojedyncze zdarzenie, które wskazuje, że wskaźnik myszy zmienił położenie. Ponieważ ustawiono znacznik wskazówki, w zdarzeniu nie ma żadnych danych na temat aktualnej pozycji myszy. Aplikacja musi sprawdzić, gdzie znajduje się wskaźnik, przy pomocy funkcji gdk_window_ get_pointer. Prosty przykład unaoczni różnicę pomiędzy używaniem zwykłych zdarzeń związanych z ruchem myszy a korzystaniem ze wskazówek. Załóżmy, że napisaliśmy aplikację (na przykład przeglądarkę cząsteczek), która rysuje znacznie bardziej skomplikowane modele, z trójwymiarowym cieniowaniem atomów. Działa doskonale na naszym najnowocześniejszym, 600-megahercowym, dwuprocesorowym komputerze, więc decydujemy się udostępnić ją reszcie świata na naszej osobistej stronie WWW. Użytkownik komputera 386SX 16MHz ściąga przeglądarkę, chcąc zanalizować kilka skomplikowanych cząsteczek. Przypuśćmy teraz, że aplikacja używa standardowych zdarzeń związanych z ruchem myszy. Nasz supernowoczesny komputer potrzebuje jednej setnej sekundy, aby
Przeglądarka cząsteczek
353
narysować model, kiedy obracamy go przy użyciu myszy. Wszystko działa świetnie, kiedy obracamy cząsteczkę w różne strony. Niestety, na 386-ce rysowanie modelu zajmuje 20 sekund, a kiedy komputer ze wszystkich sił stara się wyświetlić cząsteczkę, użytkownik przeciąga ją myszą, zastanawiając się, czemu rysowanie trwa tak długo. Po 20 sekundach cząsteczka wreszcie pojawia się na ekranie-ale w kolejce znajdują się setki zdarzeń, związanych z ruchem myszy. Nawet, jeśli użytkownik nie zrobi nic innego, każde z tych zdarzeń spowoduje przerysowanie cząsteczki, blokując komputer (przynajmniej w teorii) na długie minuty. Jeśli jednak program będzie używał wskazówek o ruchu myszy, wówczas po przerysowaniu modelu w kolejce będzie oczekiwało tylko jedno zdarzenie, informujące o zmianie położenia wskaźnika. Można sprawdzić, gdzie użytkownik przesunął mysz i odpowiednio zareagować. Oczywiście, nasz przykład jest mocno przesadzony, a osoby zajmujące się trójwymiarową grafiką na 386SX mają nierówno pod sufitem. Aby wykryć naciśnięcie lub zwolnienie klawisza myszy, musimy ustawić GDK_BUTTON_PRESS_MASK także znaczniki i GDK_BUTTON_RELEASE_MASK. Zmiany masek zdarzeń, dzięki którym program będzie otrzymywać niskopoziomowe zdarzenia, muszą być przeprowadzone przed realizacją kontrolki. Najlepszym momentem do ustawienia masek zdarzeń dla dowolnej kontrolki jest chwila tuż po jej utworzeniu. Poniższy przykład pokazuje, w jaki sposób można skonfigurować obszar rysunkowy tak, aby odbierał zdarzenia związane z ruchem myszy. GtkWidget *UtworzObszarRysunkowy () { GtkWidget *okno; /* --- Tworzymy okno najwyższego poziomu --- */ okno = gtk_window_new (GTK_WINDOW_TOPLEVEL); /* --- Tworzymy obszar rysunkowy --- */ obszar_rys = gtk_drawing_area_new (); /* --- Ustawiamy rozmiary --- */ gtk_drawing_area_size (GTK_DRAWING_AREA (obszar_rys), 300, 300); /* --- Uwidaczniamy obszar rysunkowy --- */ gtk_widget_show (obszar_rys); /* - Sygnały wykorzystywane do obsługi drugoplanowej piksmapy - */ gtk_signal_connect (GTK_OBJECT (obszar_rys), "expose_event", (GtkSignalFunc) expose_event, NULL);
Część III Rysowanie, kolor i GDK
354
gtk_signal_connect (GTK_OBJECT(obszar_rys), "configure_event", (GtkSignalFunc) configure_event, NULL); /* --- Musimy być informowani o ruchach myszy --- */ gtk_signal_connect (GTK_OBJECT (obszar_rys),"motion_notify_event", (GtkSignalFunc) motion_notify_event, NULL); /* --- Zdarzenia, które nas interesują --- */ gtk_widget_set_events (obszar_rys, GDK_EXPOSURE_MASK | GDK_LEAVE_NOTIFY_MASK | GDK_BUTTON_PRESS_MASK | GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK); return (obszar_rys); }
Aby zobaczyć efekt, jaki dają wskazówki o ruchu myszy, możemy usunąć znacznik GDK_POINTER_MOTION_HINT_MASK z wywołania funkcji gtk_widget_set_events. Oczywiście musimy wyświetlić skomplikowaną cząsteczkę, albo uruchomić komputer na powolnym komputerze, aby zaobserwować nawarstwianie się zdarzeń.
motion_notify_event Funkcja motion_notify_event wykrywa ruchy myszy i określa, czy należy przerysować cząsteczkę. W celu obrócenia cząsteczki wywoływana jest funkcja ruchMyszy. Zmienne poprzx i poprzy przechowują poprzednie położenie wskaźnika myszy; służą do obliczenia kierunku, w którym została przesunięta mysz. Znając kierunek ruchu myszy możemy zbudować właściwą macierz i obrócić cząsteczkę. Funkcja ta obsługuje zarówno wskazówki, jak i zwykłe zdarzenia związane z ruchem myszy. Jeśli obszar rysunkowy skonfigurowany jest tak, aby korzystał ze wskazówek o ruchu myszy, wówczas zdarzenie->is_hint ma wartość TRUE i funkcja będzie w każdym wywołaniu sprawdzać bieżącą pozycję wskaźnika myszy. /* * motion_notify_event * * Wywoływana podczas ruchu myszy w obrębie okna */ gint motion_notify_event (GtkWidget *kontrolka, GdkEventMotion *zdarzenie)
Przeglądarka cząsteczek
355
{ int x, y; GdkModifierType stan; /* --- Jeśli to wskazówka (kombinacja kilku zdarzeń) --- */ if (zdarzenie->is_hint) { /* --- Pobieramy nową pozycję --- */ gdk_window_get_pointer (zdarzenie->window, &x, &y, &stan); } else { /* --- Pobieramy nową pozycję --- */ x = zdarzenie->x; y = zdarzenie->y; stan = zdarzenie->state; } /* --- Jeśli wciśnięty jest przycisk myszy --- */ if (stan & GDK_BUTTON1_MASK && g->piksmapa != NULL) { /* --- Obliczamy wpływ ruchu myszy na * wyświetlaną cząsteczkę */ ruchMyszy (x, y); } /* --- Zapamiętujemy położenie myszy --- */ poprzx = x; poprzy = y; return TRUE; }
ruch Myszy Funkcja ruchMyszy przelicza macierz, służącą do obracania cząsteczki. Przypomnijmy sobie, że każdy atom ma położenie pierwotne (x, y, z), odczytane z pliku .pdb, oraz położenie przekształcone, używane podczas rysowania cząsteczki. Oba położenia są powiązane w następujący sposób: cząsteczka jest obracana przez mnożenie pierwotnego położenia atomu przez macierz atomu (amacierz), w wyniku czego otrzymujemy przekształcone położenie. Macierz atomu jest modyfikowana tak, aby uwzględnić ruch myszy. Tworzymy nową macierz (tmacierz) i zmieniamy
Część III Rysowanie, kolor i GDK
356
ją tak, aby odzwierciedlała ruch cząsteczki wokół osi x i y. Nowa macierz jest następnie mnożona przez macierz atomu, dzięki czemu macierz atomu również odzwierciedla zmiany spowodowane ruchem myszy. Pierwotne współrzędne są więc przekształcane przez macierz atomu, aby uzyskać przekształcone współrzędne atomu. Następnie wywoływana jest funkcja OdswiezCzasteczke, aby wyświetlić cząsteczkę na ekranie pod nowym kątem. /* * ruchMyszy * * Oblicza obrót cząsteczki na podstawie sposobu, * w jaki użytkownik przeciągnął mysz nad cząsteczką. * * x - położenie x myszy * x - położenie y myszy */ int ruchMyszy (int x, int y) { /* --- Obliczamy różnicę x --- */ double xtheta = (poprzy - y) * (360.0f / nSzerEkranu); /* --- Obliczamy różnicę y --- */ double ytheta = (x - poprzx) * (360.0f / nWysokEkranu); /* --- Macierz jednostkowa --- */ jednostka (tmacierz); /* --- Obracamy o różnicę -x- ruchu myszy --- */ xobrot (tmacierz, xtheta); /* --- Obracamy o różnicę -y- ruchu myszy --- */ yobrot (tmacierz, ytheta); /* --- Łączymy z bieżącym obrotem, aby uzyskać nowy --- */ mnoz (amacierz, tmacierz); /* --- Przerysowujemy z nowym obrotem --- */ OdswiezCzasteczke (); return TRUE; }
Przeglądarka cząsteczek
357
interfejs.c Plik interfejs.c zawiera funkcje służące do utworzenia głównego okna, ustawienia paska narzędziowego i menu oraz obsługi głównych zdarzeń (pochodzących od przycisków paska, menu itd.). Kod jest prawie taki sam, jak w poprzednich przykładach; dzięki kilku zmianom przeglądarka cząsteczek ma swoje własne menu i pasek narzędziowy. /* * Plik: interfejs.c * Autor: Eric Harlow * * Interfejs GUI dla przeglądarki cząsteczek * */ #include #include #include #include /* * --- Prototypy funkcji */ void PobierzNazwePliku (char *sTitle, void (*callback) (char *)); void OdswiezCzasteczke (); void CzasteczkaPokazLinie (int bWartosc); void WczytajCzasteczke (char *); GtkWidget *UtworzObszarRysunkowy (); static void UtworzGlowneOkno (); void UtworzPasek (GtkWidget *ypole); void UstawPasek (char *szPrzycisk, int nStan); void ZaznaczMenu (GtkWidget *kontrolka, gpointer dane); void OdznaczMenu (GtkWidget *kontrolka, gpointer dane); void UstawMenu (char *szPrzycisk, int nStan) ; GtkWidget *UtworzKontrolkeZXpm (GtkWidget *okno, gchar **xpm_dane); GtkWidget *UtworzElementMenu (GtkWidget *menu, char *szNazwa, char *szSkrot, char *szPodp, GtkSignalFunc funkcja, gpointer dane); GtkWidget *UtworzZaznaczalnyElement (GtkWidget *menu,
358
Część III Rysowanie, kolor i GDK
char *szNazwa, GtkSignalFunc funkcja, gpointer dane); GtkWidget *UtworzPodmenu (GtkWidget *pasekmenu, char *szNazwa); GtkWidget *UtworzPodmenuPaska (GtkWidget *menu, char *szNazwa); /* * --- Zmienne globalne */ GtkWidget *glowne_okno; GtkTooltips *podpowiedzi; GtkAccelGroup *grupa_skrotow; GtkWidget *pasek; GtkWidget *pasek_linie; GtkWidget *pasek_rgb; GtkWidget *pasek_etykiety; /* * --- Bitmapa dla przycisku "otwórz" */ static const gchar *xpm_otworz[] = { "16 16 4 1", " c None", "B c #000000000000", "Y c #FFFFFFFF0000", "y c #999999990000", " ", " BBB ", " BBBBB B BB ", " BYYYB BB ", " BYYYYYBBBBB ", " BYYYYYYYYYB ", " BYYYYYYYYYB ", " BYYYYYYYYYB ", " BYYBBBBBBBBBBB ", " BYYByyyyyyyyy B ", " BYByyyyyyyyy B ", " BYByyyyyyyyy B ", " BByyyyyyyyy B ", " BByyyyyyyyy B ", " BBBBBBBBBBB ",
Przeglądarka cząsteczek
" };
",
/* * --- Bitmapa dla przycisku "linie" */ static const char *xpm_linie[] = { "16 16 2 1", " c None", "B c #000000000000", " ", " ", " BB BB ", " BB BB ", " B B ", " B B ", " B B ", " B B ", " BB ", " BB ", " BBBB ", " BB ", " BBB ", " BB ", " ", " ", }; /* * --- Bitmapa dla przycisku "kolor" */ static const char *xpm_rgb[] = { "16 16 4 1", " c None", "R c #FF0000", "G c #00FF00", "B c #0000FF", " ", " BBBRRR ", " BBBBBRRRRR ", " BBBBBBRRRRRR ",
359
360 " BBBBBBRRRRRR ", " BBBBBBRRRRRR ", " BBBBBBBRRRRRRR ", " BBBBBBBRRRRRRR ", " BBBBBGGGGRRRRR ", " BBBGGGGGGGGRRR ", " GGGGGGGGGGGG ", " GGGGGGGGGGGG ", " GGGGGGGGGGGG ", " GGGGGGGGGG ", " GGGGGGG ", " ", }; /* * --- Bitmapa dla przycisku "etykiety" */ static const char *xpm_etykiety[] = { "16 16 4 1", " c None", "R c #FF0000", "G c #00FF00", "B c #000000", " ", " BB ", " BBBBBB ", " BBB BBB ", " BB BB ", " BB BB ", " BB BB ", " BB BB ", " BBBBBBBBBBBB ", " BBBBBBBBBBBB ", " BB BB ", " BB BB ", " BB BB ", " BB BB ", " BB BB ", " ", };
Część III Rysowanie, kolor i GDK
Przeglądarka cząsteczek
/* * KoniecProgramu * * Wyjście z programu */ void KoniecProgramu () { gtk_main_quit (); } /* * main * * --- Program zaczyna się tutaj */ int main(int argc, char *argv[]) { /* --- Inicjacja GTK --- */ gtk_init (&argc, &argv); /* --- Inicjacja podpowiedzi --- */ podpowiedzi = gtk_tooltips_new (); /* --- Tworzymy okno --- */ UtworzGlowneOkno (); /* --- Wczytujemy domyślną cząsteczkę --- */ WczytajCzasteczke ("molecule.pdb"); /* --- Główna pętla obsługi zdarzeń --- */ gtk_main(); return 0; } /* * PokazEtykiety * * Czy jest wciśnięty przycisk "pokaż etykiety" na * pasku narzędziowym, który wskazuje, że użytkownik * chce wyświetlić nazwy atomów? */ int PokazEtykiety ()
361
Część III Rysowanie, kolor i GDK
362 {
return (GTK_TOGGLE_BUTTON (pasek_etykiety)->active); } /* * PokazRGB * * Czy jest wciśnięty przycisk "pokaż kolory" na * pasku narzędziowym, który wskazuje, że użytkownik * chce wyświetlić atomy w kolorze? */ int PokazRGB () { return (GTK_TOGGLE_BUTTON (pasek_rgb)->active); } /* * PokazLinie * * Wskazuje, czy pomiędzy atomami należy rysować linie, * które przedstawiają wiązania w cząsteczce. */ int PokazLinie () { return (GTK_TOGGLE_BUTTON (pasek_linie)->active); } /* * OtworzPlik * * Otwiera plik .pdb */ void OtworzPlik (GtkWidget *kontrolka, gpointer dane) { PobierzNazwePliku ("Otwórz cząsteczkę", WczytajCzasteczke); } /* * UtworzGlowneOkno *
Przeglądarka cząsteczek
363
* Tworzy główne okno i związane z nim menu/paski narzędziowe. */ static void UtworzGlowneOkno () { GtkWidget *kontrolka; GtkWidget *ypole; GtkWidget *pasekmenu; GtkWidget *menu; GtkWidget *elmenu; /* --- Tworzymy główne okno i ustawiamy jego rozmiary --- */ glowne_okno = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_widget_set_usize(glowne_okno, 360, 260); gtk_window_set_title (GTK_WINDOW (glowne_okno), "Przeglądarka cząsteczek"); /* gtk_container_border_width (GTK_CONTAINER (glowne_okno), 0); */ /* --- Tworzymy skróty klawiszowe --- */ grupa_skrotow = gtk_accel_group_new(); gtk_accel_group_attach (grupa_skrotow, GTK_OBJECT (glowne_okno)); /* -- Okno najwyższego poziomu musi czekać na sygnał destroy -- */ gtk_signal_connect (GTK_OBJECT (glowne_okno), "destroy", GTK_SIGNAL_FUNC(KoniecProgramu), NULL); /* --- Tworzymy pole pakujące na menu i pasek narzędziowy --- */ ypole = gtk_vbox_new (FALSE, 0); /* --- Pokazujemy pole pakujące --- */ gtk_container_add (GTK_CONTAINER (glowne_okno), ypole); gtk_widget_show (ypole); gtk_widget_show (glowne_okno); /* --- Pasek menu --- */ pasekmenu = gtk_menu_bar_new (); gtk_box_pack_start (GTK_BOX (ypole), pasekmenu, FALSE, TRUE, 0); gtk_widget_show (pasekmenu); /* ------------------- Menu Plik ------------------- */ menu = UtworzPodmenuPaska (pasekmenu, "Plik");
Część III Rysowanie, kolor i GDK
364
elmenu = UtworzElementMenu (menu, "Otwórz", "^O", "Otwiera cząsteczkę", GTK_SIGNAL_FUNC (OtworzPlik), "otwórz"); elmenu = UtworzElementMenu (menu, NULL, NULL, NULL, NULL, NULL); elmenu = UtworzElementMenu (menu, "Zakończ", "", "Czy jest bardziej wymowna opcja?", GTK_SIGNAL_FUNC (KoniecProgramu), "zakończ"); /* --- Tworzymy pasek narzędziowy --- */ UtworzPasek (ypole); kontrolka = UtworzObszarRysunkowy (); gtk_box_pack_start (GTK_BOX (ypole), kontrolka, TRUE, TRUE, 0); } /* * UtworzPasek * * Tworzy pasek narzędziowy */ void UtworzPasek (GtkWidget *ypole) { /* --- Tworzymy pasek i dodajemy go do okna --- */ pasek = gtk_toolbar_new (GTK_ORIENTATION_HORIZONTAL, GTK_TOOLBAR_ICONS); gtk_box_pack_start (GTK_BOX (ypole), pasek, FALSE, TRUE, 0); gtk_widget_show (pasek); /* --- Tworzymy przycisk "otwórz" --- */ gtk_toolbar_append_item (GTK_TOOLBAR (pasek), "Okno dialogowe Otwórz", "Okno dialogowe Otwórz", "", UtworzKontrolkeZXpm (ypole, (gchar **) xpm_otworz), (GtkSignalFunc) OtworzPlik, NULL); /* --- Niewielki odstęp --- */ gtk_toolbar_append_space (GTK_TOOLBAR (pasek)); pasek_linie = gtk_toolbar_append_element (GTK_TOOLBAR (pasek), GTK_TOOLBAR_CHILD_TOGGLEBUTTON,
Przeglądarka cząsteczek
365
NULL, "Wiązania", "Wiązania", "Wiązania", UtworzKontrolkeZXpm (ypole, (gchar **) xpm_linie), (GtkSignalFunc) OdswiezCzasteczke, NULL); pasek_rgb = gtk_toolbar_append_element (GTK_TOOLBAR (pasek), GTK_TOOLBAR_CHILD_TOGGLEBUTTON, NULL, "Kolory", "Kolory", "Kolory", UtworzKontrolkeZXpm (ypole, (gchar **) xpm_rgb), (GtkSignalFunc) OdswiezCzasteczke, NULL); pasek_etykiety = gtk_toolbar_append_element (GTK_TOOLBAR (pasek), GTK_TOOLBAR_CHILD_TOGGLEBUTTON, NULL, "Nazwy atomów", "Nazwy atomów", "Nazwy atomów", UtworzKontrolkeZXpm (ypole, (gchar **) xpm_etykiety), (GtkSignalFunc) OdswiezCzasteczke, NULL); }
macierz 3d.c W pliku macierz3d.c znajdują się funkcje służące do mnożenia macierzy. Jeśli czytelnik byłby zainteresowany bliższym poznaniem arytmetyki trójwymiarowej, zachęcamy do sięgnięcia po odpowiednią książkę (mnożenie macierzy powinna wyjaśnić książka poświęcona algebrze liniowej, a także wiele książek traktujących o grafice 3D). Załączamy tutaj kod dla pełnego obrazu. /* * Plik: macierz3d.c * Autor: Eric Harlow * * Konwersja z pliku klasy Javy Matrix 3D */ #include "atom.h" #include "macierz3d.h" #include
Część III Rysowanie, kolor i GDK
366 static double pi = 3.14159265; /* * NowaMacierz3D * * Tworzy nową macierz */ typMacierz3D *NowaMacierz3D () { typMacierz3D *macierz;
macierz = (typMacierz3D *) g_malloc (sizeof (typMacierz3D)); jednostka (macierz); return (macierz); } /* * skaluj * * Skaluje obiekt */ void sksluj (typMacierz3D *macierz, double f) { macierz->xx *= f; macierz->xy *= f; macierz->xz *= f; macierz->xo *= f; macierz->yx *= f; macierz->yy *= f; macierz->yz *= f; macierz->yo *= f; macierz->zx *= f; macierz->zy *= f; macierz->zz *= f; macierz->zo *= f; } /* * skaluj3 * * Skaluje każdy kierunek o inny czynnik
Przeglądarka cząsteczek
*/ void skaluj3 (typMacierz3D *macierz, double xf, double yf, double zf) { macierz->xx *= xf; macierz->xy *= xf; macierz->xz *= xf; macierz->xo *= xf; macierz->yx *= yf; macierz->yy *= yf; macierz->yz *= yf; macierz->yo *= yf; macierz->zx *= zf; macierz->zy *= zf; macierz->zz *= zf; macierz->zo *= zf; } /* * przestaw * * Przesuwa punkt reprezentowany przez macierz o (x, y, z) */ void przestaw (typMacierz3D *macierz, double x, double y, double z) { macierz->xo += x; macierz->yo += y; macierz->zo += z; } /* * yobrot * * Dodaje do macierzy obrót wokół osi y o kąt (theta). */ void yobrot (typMacierz3D *macierz, double theta) { double ct; double st; double Nxx; double Nxy; double Nxz;
367
Część III Rysowanie, kolor i GDK
368 double Nxo; double Nzx; double Nzy; double Nzz; double Nzo; theta *= (pi / 180); ct = cos (theta); st = sin (theta);
Nxx = (double) (macierz->xx * ct + macierz->zx * st); Nxy = (double) (macierz->xy * ct + macierz->zy * st); Nxz = (double) (macierz->xz * ct + macierz->zz * st); Nxo = (double) (macierz->xo * ct + macierz->zo * st); Nzx = (double) (macierz->zx * ct - macierz->xx * st); Nzy = (double) (macierz->zy * ct - macierz->xy * st); Nzz = (double) (macierz->zz * ct - macierz->xz * st); Nzo = (double) (macierz->zo * ct - macierz->xo * st); macierz->xo = Nxo; macierz->xx = Nxx; macierz->xy = Nxy; macierz->xz = Nxz; macierz->zo = Nzo; macierz->zx = Nzx; macierz->zy = Nzy; macierz->zz = Nzz; } /* * xobrot * * Dodaje do macierzy obrót wokół osi x o kąt (theta). */ void xobrot (typMacierz3D *macierz, double theta) { double ct; double st; double Nyx; double Nyy; double Nyz;
Przeglądarka cząsteczek
double Nyo; double Nzx; double Nzy; double Nzz; double Nzo; theta *= (pi / 180); ct = cos (theta); st = sin (theta); Nyx = (double) (macierz->yx * ct + macierz->zx * st); Nyy = (double) (macierz->yy * ct + macierz->zy * st); Nyz = (double) (macierz->yz * ct + macierz->zz * st); Nyo = (double) (macierz->yo * ct + macierz->zo * st); Nzx = (double) (macierz->zx * ct - macierz->yx * st); Nzy = (double) (macierz->zy * ct - macierz->yy * st); Nzz = (double) (macierz->zz * ct - macierz->yz * st); Nzo = (double) (macierz->zo * ct - macierz->yo * st); macierz->yo = Nyo; macierz->yx = Nyx; macierz->yy = Nyy; macierz->yz = Nyz; macierz->zo = Nzo; macierz->zx = Nzx; macierz->zy = Nzy; macierz->zz = Nzz; } /* * zobrot * * Dodaje do macierzy obrót wokół osi z o kąt (theta). */ void zobrot (typMacierz3D *macierz, double theta) { double ct; double st; double Nyx; double Nyy; double Nyz;
369
Część III Rysowanie, kolor i GDK
370 double Nyo; double Nxx; double Nxy; double Nxz; double Nxo; theta *= (pi / 180); ct = cos(theta); st = sin(theta);
Nyx = (double) (macierz->yx * ct + macierz->xx * st); Nyy = (double) (macierz->yy * ct + macierz->xy * st); Nyz = (double) (macierz->yz * ct + macierz->xz * st); Nyo = (double) (macierz->yo * ct + macierz->xo * st); Nxx = (double) (macierz->xx * ct - macierz->yx * st); Nxy = (double) (macierz->xy * ct - macierz->yy * st); Nxz = (double) (macierz->xz * ct - macierz->yz * st); Nxo = (double) (macierz->xo * ct - macierz->yo * st); macierz->yo = Nyo; macierz->yx = Nyx; macierz->yy = Nyy; macierz->yz = Nyz; macierz->xo = Nxo; macierz->xx = Nxx; macierz->xy = Nxy; macierz->xz = Nxz; } /* * mnoz * * Mnoży pierwszą macierz przez drugą. * Nowa wartość jest zachowywana w pierwszej macierzy. */ void mnoz (typMacierz3D *macierz, typMacierz3D *rhs) { double lxx = macierz->xx * rhs->xx + macierz->yx * rhs->xy + macierz->zx * rhs->xz; double lxy = macierz->xy * rhs->xx +
Przeglądarka cząsteczek
macierz->yy * rhs->xy + macierz->zy * rhs->xz; double lxz = macierz->xz * rhs->xx + macierz->yz * rhs->xy + macierz->zz * rhs->xz; double lxo = macierz->xo * rhs->xx + macierz->yo * rhs->xy + macierz->zo * rhs->xz + rhs->xo; double lyx = macierz->xx * rhs->yx + macierz->yx * rhs->yy + macierz->zx * rhs->yz; double lyy = macierz->xy * rhs->yx + macierz->yy * rhs->yy + macierz->zy * rhs->yz; double lyz = macierz->xz * rhs->yx + macierz->yz * rhs->yy + macierz->zz * rhs->yz; double lyo = macierz->xo * rhs->yx + macierz->yo * rhs->yy + macierz->zo * rhs->yz + rhs->yo; double lzx = macierz->xx * rhs->zx + macierz->yx * rhs->zy + macierz->zx * rhs->zz; double lzy = macierz->xy * rhs->zx + macierz->yy * rhs->zy + macierz->zy * rhs->zz; double lzz = macierz->xz * rhs->zx + macierz->yz * rhs->zy + macierz->zz * rhs->zz; double lzo = macierz->xo * rhs->zx + macierz->yo * rhs->zy + macierz->zo * rhs->zz + rhs->zo; macierz->xx = lxx; macierz->xy = lxy; macierz->xz = lxz; macierz->xo = lxo; macierz->yx = lyx; macierz->yy = lyy;
371
Część III Rysowanie, kolor i GDK
372 macierz->yz = lyz; macierz->yo = lyo; macierz->zx = lzx; macierz->zy = lzy; macierz->zz = lzz; macierz->zo = lzo; } /* * jednostka * * Czyni macierz macierzą jednostkową */ void jednostka (typMacierz3D *macierz) { macierz->xo = 0; macierz->xx = 1; macierz->xy = 0; macierz->xz = 0; macierz->yo = 0; macierz->yx = 0; macierz->yy = 1; macierz->yz = 0; macierz->zo = 0; macierz->zx = 0; macierz->zy = 0; macierz->zz = 1; }
/* * Transformuj * * Poddaje translacji współrzędne atomu */ void Transformuj (typMacierz3D *macierz, typAtom *atom) { double lxx = macierz->xx, lxy = macierz->xy, lxz = macierz->xz, lxo = macierz->xo; double lyx = macierz->yx, lyy = macierz->yy, lyz = macierz->yz, lyo = macierz->yo;
Przeglądarka cząsteczek
373
double lzx = macierz->zx, lzy = macierz->zy, lzz = macierz->zz, lzo = macierz->zo; double x = atom->x; double y = atom->y; double z = atom->z; atom->tx = (x * lxx + y * lxy + z * lxz + lxo); atom->ty = (x * lyx + y * lyy + z * lyz + lyo); atom->tz = (x * lzx + y * lzy + z * lzz + lzo); }
Pozostała część kodu Pozostała część kodu została wzięta z innych rozdziałów. Plik rozne.c pochodzi z rozdziału 5, „Menu, paski narzędziowe i podpowiedzi” i pomaga w tworzeniu menu i paska narzędziowego. Kod wybpliku.c pochodzi z rozdziału 6, „Dalsze kontrolki: ramki, tekst, okna dialogowe, okno wyboru pliku i pasek postępów”. Moduły te nie wymagały wielu zmian (jeśli w ogóle) w celu dostosowania ich do pracy w przeglądarce cząsteczek. Zmieniono w niewielkim stopniu wybpliku.c, aby uczynić go bardziej uniwersalnym; teraz można wykorzystywać go w dowolnym programie (co, jak wiadomo, jest oznaką modularności kodu).
Podsumowanie Przeglądarka cząsteczek jest bardziej skomplikowanym przykładem aplikacji stworzonej przy użyciu GDK. Ilustruje ona sposób interakcji użytkownika z kontrolką obszaru rysunkowego GDK. Do obracania cząsteczki wykorzystaliśmy zdarzenia związane z ruchem myszy. Rozdział omówił niektóre spośród niskopoziomowych zdarzeń, których obsługa jest konieczna, aby możliwe było obracanie cząsteczek przy pomocy myszy.
Rozdział 13 Duszki i animacja GDK (Graphics Drawing Kit) potrafi więcej, niż tylko rysować linie i prostokąty. Przy naszej niewielkiej pomocy może również zajmować się animacją, co można wykorzystać w grach. GDK nie jest szczególnie wydajną biblioteką do pisania gier, więc próba stworzenia przy jej użyciu kolejnego Dooma albo Quake’a ma niewielkie szanse powodzenia. Jednakże gry, które nie mają wielkich wymagań co do wydajności, mogą korzystać z GDK.
Animacja Animacja w grach komputerowych polega na szybkim przesuwaniu rysunków po ekranie. Mogą to być statki kosmiczne, ludziki, albo inne obiekty, które mogą reprezentować gracza i komputer. Takie ruchome rysunki często bywają nazywane duszkami (sprites). Animacja wygląda najlepiej, jeśli rysunki na ekranie nie migoczą. Najłatwiejszą metodą poradzenia sobie z tym problemem jest użycie podwójnego buforowania (przypomnijmy sobie przykładowy program zegara z rozdziału 10). Sekwencje animacji w naszych przykładach będą tworzone przy pomocy piksmap, ponieważ ich format jest już nam znany i łatwo jest je rysować z uwzględnieniem przezroczystych obszarów, przez które widać tło. Animowane sekwencje tworzymy w trzech krokach. Najpierw należy wyczyścić tło i narysować drugoplanową scenerię. Następnie należy umieścić w tle rysunki, które będziemy animować. Trzeci krok polega na skopiowaniu narysowanej klatki animacji do okna w celu wyświetlenia jej na ekranie.
Używanie duszków Aby przeprowadzić animację, musimy najpierw zdefiniować duszka jako rysunek xpm, z którego możemy uzyskać nadającą się do wyświetlenia piksmapę i maskę. Maska definiuje obszar, na którym będziemy rysować. Rysunki są prostokątne, ale nie chcemy rysować całego prostokąta-lepiej jest zamaskować obszar rysunku i rysować tylko w tych miejscach, gdzie w rysunku są jakieś dane.
Część III Rysowanie, kolor i GDK
376
Chodzącego człowieka można stworzyć przy pomocy trzech rysunków-nogi razem, lewa noga w przedzie i prawa noga w przedzie. Rysunek ze złączonymi nogami można wykorzystać dwukrotnie, jako fazę przejściową pomiędzy rysunkami „lewa noga w przedzie” – „prawa noga w przedzie” oraz „prawa noga w przedzie” – „lewa noga w przedzie”. W jednej ręce człowiek niesie książkę, aby było widać ruch jego ramienia. Używane obrazki wyglądają następująco: static char *xpm_stoi[] = { "24 24 4 1", " c None", "X c #FFFFFF", ". c #000000", "b c #0000FF", "
......
"
.XXXXXX.
",
"
.XXXXX.XX.
"
.XXXXXXXX.
"
.XXXXXX.
", ", ", ",
"
.XX.
"
.XXXX.
",
"
.X.XX.
",
"
.X.XX.
",
"
.X.XX.
",
"
.X.XX.
",
"
.X.XX.
",
"
.X.XX.
",
"
.b..
",
"
.bb.
",
"
.bb.
",
"
.bb.
",
"
.bb.
",
"
.bb.
",
"
.bb.
",
"
.bb...
",
"
.XXXXX.
",
"
.XXXXX.
",
"
.....
",
",
}; static char *xpm_idzie1[] = { "24 24 4 1",
Duszki i animacja
377
" c None", "X c #FFFFFF", ". c #000000", "b c #0000FF", "
......
"
.XXXXXX.
",
"
.XXXXX.XX.
"
.XXXXXXXX.
"
.XXXXXX.
", ", ", ",
"
.XX.
"
.XXXX.
", ",
"
.XXXX.
",
"
.X.XX.X.
",
"
.X.XX.X..
",
"
.X..XX..XX.
"
.X..XX.XXXX.
"
. .XX..XX.
", ", ",
"
.bb. ..
",
"
..b.
",
"
.b..b.
",
"
.b..b.
",
"
.b. .b.
",
"
.b. .b.
",
"
.b. .b.
",
"
.b. .b...
",
"
.XXX. .XXX.
",
"
.XXX. .XXX.
",
"
... ...
",
}; static char *xpm_idzie2[] = { "24 24 4 1", " c None", "X c #FFFFFF", ". c #000000", "b c #0000FF", "
......
"
.XXXXXX.
",
"
.XXXXX.XX.
"
.XXXXXXXX.
", ", ",
Część III Rysowanie, kolor i GDK
378 "
.XXXXXX.
",
"
.XX.
"
.XXXX.
", ",
"
.XXXX.
",
"
.X.XX.X.
",
"
..X.XX.X.
",
"
.XX. XX..X.
"
.XXXX.XX..X.
"
.XX..XX..
"
.. .bb.
",
"
.b..
",
"
.b..b.
",
"
.b..b.
",
"
.b. .b.
",
"
.b. .b.
",
"
.b. .b.
",
"
.b.. .b..
",
"
.XXX. .XXX.
",
"
.XXX. .XXX.
",
"
... ...
", ", ",
",
};
Rysunki będziemy przechowywać w strukturze danych duszka, zdefiniowanej następująco: typedef struct { char **dane_xpm; GdkPixmap *piksmapa; GdkBitmap *maska; } typDuszek;
Pole dane_xpm przechowuje dane xpm piksmapy, a pola piksmapa i maska przechowują rezultaty wywołania funkcji gdk_pixmap_create_from_xpm_d. Chodzący mężczyzna będzie opisany przez tablicę elementów typDuszek, w której będą przechowywane przedstawione wyżej rysunki. Pola piksmapa i maska struktury są początkowo ustawiane na NULL. /* * Chodzący mężczyzna */ typDuszek duszek_pan[] = {
Duszki i animacja
379
{ xpm_stoi, NULL, NULL }, { xpm_idzie1, NULL, NULL }, { xpm_stoi, NULL, NULL }, { xpm_idzie2, NULL, NULL }, { NULL, NULL, NULL } };
Tablica duszka definiuje rysunki oraz kolejność, w jakiej powinny być wyświetlane. Struktura typAktor definiuje położenie duszka na ekranie, numer kolejny obecnie wyświetlanego rysunku, oraz rozmiar rysunku (zakładamy, że wszystkie rysunki w sekwencji mają te same rozmiary). typedef struct { int sekw; int x; int y; int wysok; int szerok; int nSekwencje; typDuszek *duszki; } typAktor;
Ładowanie rysunków Funkcja LadujPiksmapy dołącza do struktury typAktor elementy typDuszek, aby aktor wiedział, który duszek jest wyświetlany i w jakim punkcie ekranu. Wywołujemy funkcję wraz z oknem, aktorem i tablicą duszków, stanowiących poszczególne klatki ruchu aktora. Poniżej wiążemy z aktorem tablicę duszków duszek_pan i inicjujemy strukturę aktora: LadujPiksmapy (okno, &pan, duszek_pan);
Kod funkcji LadujPiksmapy zamienia każdy rysunek xpm w piksmapę i maskę. Tablica duszków jest przechowywana w strukturze typAktor, wraz z liczbą duszków w sekwencji oraz rozmiarami duszków. /* * LadujPiksmapy * * Ładuje piksmapy aktorów, pobiera informacje o duszkach * z danych xpm i inicjuje dane animacji aktorów. */ void LadujPiksmapy (GtkWidget *kontrolka, typAktor *aktor,
Część III Rysowanie, kolor i GDK
380 typDuszek *duszki) { int i = 0; /* --- Pobieramy każdą klatkę --- */ while (duszki[i].dane_xpm) {
/* --- Zamieniamy dane xpm na piksmapę i maskę --- */ duszki[i].piksmapa = gdk_pixmap_create_from_xpm_d ( kontrolka->window, &duszki[i].maska, NULL, duszki[i].dane_xpm); i++; } /* --- Pobieramy wysokość i szerokość duszka --- */ PobierzSzerIWys (duszki[0].dane_xpm, &aktor->szerok, &aktor->wysok); /* --- Inicjacja danych duszka --- */ aktor->sekw = 0; aktor->nSekwencje = i; aktor->duszki = duszki; }
Wyświetlanie rysunków Po powiązaniu wszystkich aktorów z duszkami możemy zacząć animację. Podobnie jak w innych przykładach z podwójnym buforowaniem, procedura przerysowująca operuje na drugoplanowej piksmapie. Kiedy rysowanie dobiegnie końca, piksmapa jest kopiowana do obszaru rysunkowego. /* * Przerysuj * * dane - kontrolka do przerysowania */ gint Przerysuj (gpointer dane) { GtkWidget* obszar_rys = (GtkWidget *) dane; GdkRectangle uakt_prostokat;
Duszki i animacja
static przesuniecie = 0; int nGora; /* --- czyścimy piskmapę (rysunek drugoplanowy) --- */ gdk_draw_rectangle (piksmapa, obszar_rys->style->black_gc, TRUE, 0, 0, obszar_rys->allocation.width, obszar_rys->allocation.height); /* --- Rysujemy ulicę --- */ gdk_draw_rectangle (piksmapa, obszar_rys->style->white_gc, TRUE, 50, 0, 100, obszar_rys->allocation.height); /* * Rysujemy linie na jezdni */ /* --- Ustalamy, gdzie powinna być pierwsza linia --- */ przesuniecie++; if ((przesuniecie - DLUG_PRZERWY) >= 0) { przesuniecie -= (DLUG_LINII + DLUG_PRZERWY); } /* --- Rysujemy wszystkie linie na jezdni --- */ nGora = przesuniecie; do { gdk_draw_rectangle (piksmapa, obszar_rys->style->black_gc, TRUE, 100, nGora, 5, DLUG_LINII); nGora += DLUG_LINII + DLUG_PRZERWY; } while (nGora < obszar_rys->allocation.height); /* --- Rysujemy wszystkie duszki --- */ KlatkaAnimacji (obszar_rys, &rower, 3); KlatkaAnimacji (obszar_rys, &pan, 2);
381
Część III Rysowanie, kolor i GDK
382 KlatkaAnimacji (obszar_rys, &pani, 2); KlatkaAnimacji (obszar_rys, &policja, 0); KlatkaAnimacji (obszar_rys, &pilka1, 2); KlatkaAnimacji (obszar_rys, &auto1, 3); /* --- Trzeba uaktualnić cały ekran --- */ uakt_prostokat.x = 0; uakt_prostokat.y = 0;
uakt_prostokat.width = obszar_rys->allocation.width; uakt_prostokat.height = obszar_rys->allocation.height; /* --- Uaktualniamy go --- */ gtk_widget_draw (obszar_rys, &uakt_prostokat); }
Funkcja KlatkaAnimacji rysuje każdego animowanego duszka na drugoplanowej piksmapie przy pomocy funkcji gdk_draw_pixmap. Zwykle rysowanie duszka spowodowałoby narysowanie całego zawierającego go prostokąta (patrz rysunek 13.1). Można na szczęście pozbyć się tego efektu, używając maski, zwróconej przez funkcję gdk_pixmap_create_from_xpm_d. Maska tworzona jest na podstawie kolorów, zdefiniowanych w danych xpm. Jedynym kolorem, który nie jest uwzględniany w masce, jest kolor None; staje się on przezroczystym kolorem maski. Funkcja gdk_gc_set_clip_mask ustawia maskę, która zostanie wykorzystana w funkcji gdk_draw_pixmap. Maska musi zostać umieszczona w punkcie, gdzie będzie rysowana piksmapa, przy pomocy funkcji gdk_gc_set_clip_origin. Dzięki tym dwóm funkcjom pozbywamy się prostokąta wokół umieszczanego na ekranie rysunku (patrz rysunek 13.2).
Rysunek 13.1. Duszki bez przezroczysto-
Duszki i animacja
383 ści. Rysunek 13.2. Duszki z przezroczystością.
Funkcja KlatkaAnimacji przyjmuje aktora, uaktualnia jego położenie, dodając odległość (nOdlegl) do jego współrzędnej x i rysuje aktora na ekranie. Rysowanie może odbywać się albo przy pomocy maski, albo bez niej-określa do znacznik bUzyjMaski. Rysowanie bez maski sprawia, że wokół duszka pojawia się duży prostokąt (patrz rysunek 13.2). Oczywiście, jeśli podczas rysowania duszka używaliśmy maski, powinniśmy po zakończeniu rysowania ustawić ją na NULL, ponieważ nie jest już potrzebna. Oto pełny kod funkcji, wyświetlającej całą sekwencję animacji: /* * KlatkaAnimacji * * Przechodzi do następnego duszka w sekwencji * i rysuje go przy użyciu maski. */ KlatkaAnimacji (GtkWidget *obszar_rys, typAktor *aktor, int nOdlegl) { GdkGC *gc; aktor->x += nOdlegl; if (aktor->x > obszar_rys->allocation.width) { aktor->x = 0; } /* --- Używamy następnego rysunku w sekwencji --- */ aktor->sekw++; /* --- Jeśli jesteśmy na końcu sekwencji, używamy 0 --- */ if (aktor->sekw >= aktor->nSekwencje) { aktor->sekw = 0; } /* --- Pobieramy kolor pierwszoplanowy --- */ gc = obszar_rys->style->fg_gc[GTK_STATE_NORMAL]; if (bUzyjMaski) { /* --- Ustawiamy przycinanie duszków --- */ gdk_gc_set_clip_mask (gc, aktor->duszki[aktor->sekw].maska); /* --- Ustwiamy punkt początkowy przycinania --- */
Część III Rysowanie, kolor i GDK
384
gdk_gc_set_clip_origin (gc, aktor->x, aktor->y); } /* --- Kopiujemy odpowiednio przyciętą piksmapę do okna --- */ gdk_draw_pixmap (piksmapa, obszar_rys->style->fg_gc[GTK_STATE_NORMAL], aktor->duszki[aktor->sekw].piksmapa, 0, 0, aktor->x, aktor->y, aktor->szerok, aktor->wysok); if (bUzyjMaski) { /* --- Czyścimy maskę przycinania --- */ gdk_gc_set_clip_mask (gc, NULL); } }
Pełny kod źródłowy Poniżej umieszczamy pełny program, demonstrujący animację. Zawiera on kilka animowanych sekwencji, które przesuwają się przez okno programu. Większość duszków składa się z kilku kolejnych piksmap, dzięki którym wydają się być w ruchu. Chodzący mężczyzna stawia kroki, a rowerzysta podczas jazdy kręci pedałami. Duszek piłki jest przykładowym rysunkiem zawierającym wewnątrz przezroczyste obszary - kiedy piłka porusza się nad tłem i innymi duszkami, można przez nią dojrzeć inne obiekty. /* * Autor: Eric Harlow * * Tworzenie aplikacji w Linuksie * Demonstracja duszków */ #include #include #include "pan.h" #include "pani.h" #include "pilka.h" #include "auto.h"
Duszki i animacja
#include "policja.h" #include "rower.h" #define DLUG_LINII 20 #define DLUG_PRZERWY 15 /* * Struktura z danymi duszka */ typedef struct { char **dane_xpm; GdkPixmap *piksmapa; GdkBitmap *maska; } typDuszek; /* * Struktura aktora. Aktor składa się z jednego * lub kilku duszków. */ typedef struct { int sekw; int x; int y; int wysok; int szerok; int nSekwencje; typDuszek *duszki; } typAktor; /* * Chodzący mężczyzna */ typDuszek duszek_pan[] = { { xpm_stoi, NULL, NULL }, { xpm_idzie1, NULL, NULL }, { xpm_stoi, NULL, NULL }, { xpm_idzie2, NULL, NULL }, { NULL, NULL, NULL }
385
Część III Rysowanie, kolor i GDK
386 }; /* * Mężczyzna na rowerze */ typDuszek duszek_rower[] = { { xpm_rower1, NULL, NULL }, { xpm_rower2, NULL, NULL }, { xpm_rower3, NULL, NULL }, { NULL, NULL, NULL } }; /* * Chodząca kobieta */ typDuszek duszek_pani[] = { { xpm_pani, NULL, NULL }, { xpm_paniidzie1, NULL, NULL }, { xpm_panistoi, NULL, NULL }, { xpm_paniidzie2, NULL, NULL }, { NULL, NULL, NULL } }; /* * Samochód policyjny */ typDuszek duszek_policja[] = { { xpm_policja1, NULL, NULL }, { xpm_policja2, NULL, NULL }, { NULL, NULL, NULL } }; /* * Częściowo przezroczysta piłka */ typDuszek duszek_pilka[] = { { xpm_pilka1, NULL, NULL }, { NULL, NULL, NULL } }; /*
Duszki i animacja
* Samochód */ typDuszek duszek_auto[] = { { xpm_auto1, NULL, NULL }, { xpm_auto1, NULL, NULL }, { NULL, NULL, NULL } }; /* * Oto gwiazdy naszego filmu */ typAktor pan; typAktor rower; typAktor pani; typAktor policja; typAktor pilka1; typAktor auto1; /* --- Drugoplanowa piksmapa dla obszaru rysunkowego --- */ static GdkPixmap *piksmapa = NULL; /* --- Znacznik używania maski --- */ static int bUzyjMaski = TRUE; /* * Prototypy. */ void PobierzSzerIWys (gchar **xpm, int *szerok, int *wysok); /* * LadujPiksmapy * * Ładuje piksmapy aktorów, pobiera informacje o duszkach * z danych xpm i inicjuje dane animacji aktorów. */ void LadujPiksmapy (GtkWidget *kontrolka, typAktor *aktor, typDuszek *duszki) { int i = 0; /* --- Pobieramy każdą klatkę --- */
387
Część III Rysowanie, kolor i GDK
388 while (duszki[i].dane_xpm) {
/* --- Zamieniamy dane xpm na piksmapę i maskę --- */ duszki[i].piksmapa = gdk_pixmap_create_from_xpm_d ( kontrolka->window, &duszki[i].maska, NULL, duszki[i].dane_xpm); i++; } /* --- Pobieramy wysokość i szerokość duszka --- */ PobierzSzerIWys (duszki[0].dane_xpm, &aktor->szerok, &aktor->wysok); /* --- Inicjacja danych duszka --- */ aktor->sekw = 0; aktor->nSekwencje = i; aktor->duszki = duszki; } /* * KlatkaAnimacji * * Przechodzi do następnego duszka w sekwencji * i rysuje go przy użyciu maski. */ KlatkaAnimacji (GtkWidget *obszar_rys, typAktor *aktor, int nOdlegl) { GdkGC *gc; aktor->x += nOdlegl; if (aktor->x > obszar_rys->allocation.width) { aktor->x = 0; } /* --- Używamy następnego rysunku w sekwencji --- */ aktor->sekw++; /* --- Jeśli jesteśmy na końcu sekwencji, używamy 0 --- */ if (aktor->sekw >= aktor->nSekwencje) { aktor->sekw = 0; }
Duszki i animacja
/* --- Pobieramy kolor pierwszoplanowy --- */ gc = obszar_rys->style->fg_gc[GTK_STATE_NORMAL]; if (bUzyjMaski) { /* --- Ustawiamy przycinanie duszków --- */ gdk_gc_set_clip_mask (gc, aktor->duszki[aktor->sekw].maska); /* --- Ustwiamy punkt początkowy przycinania --- */ gdk_gc_set_clip_origin (gc, aktor->x, aktor->y); } /* --- Kopiujemy odpowiednio przyciętą piksmapę do okna --- */ gdk_draw_pixmap (piksmapa, obszar_rys->style->fg_gc[GTK_STATE_NORMAL], aktor->duszki[aktor->sekw].piksmapa, 0, 0, aktor->x, aktor->y, aktor->szerok, aktor->wysok); if (bUzyjMaski) { /* --- Czyścimy maskę przycinania --- */ gdk_gc_set_clip_mask (gc, NULL); } } /* * Przerysuj * * dane - kontrolka do przerysowania */ gint Przerysuj (gpointer dane) { GtkWidget* obszar_rys = (GtkWidget *) dane; GdkRectangle uakt_prostokat; static przesuniecie = 0; int nGora; /* --- czyścimy piskmapę (rysunek drugoplanowy) --- */ gdk_draw_rectangle (piksmapa, obszar_rys->style->black_gc,
389
Część III Rysowanie, kolor i GDK
390 TRUE, 0, 0, obszar_rys->allocation.width, obszar_rys->allocation.height); /* --- Rysujemy ulicę --- */ gdk_draw_rectangle (piksmapa, obszar_rys->style->white_gc, TRUE, 50, 0, 100, obszar_rys->allocation.height); /* * Rysujemy linie na jezdni */
/* --- Ustalamy, gdzie powinna być pierwsza linia --- */ przesuniecie++; if ((przesuniecie - DLUG_PRZERWY) >= 0) { przesuniecie -= (DLUG_LINII + DLUG_PRZERWY); } /* --- Rysujemy wszystkie linie na jezdni --- */ nGora = przesuniecie; do { gdk_draw_rectangle (piksmapa, obszar_rys->style->black_gc, TRUE, 100, nGora, 5, DLUG_LINII); nGora += DLUG_LINII + DLUG_PRZERWY; } while (nGora < obszar_rys->allocation.height); /* --- Rysujemy wszystkie duszki --- */ KlatkaAnimacji (obszar_rys, &rower, 3); KlatkaAnimacji (obszar_rys, &pan, 2); KlatkaAnimacji (obszar_rys, &pani, 2); KlatkaAnimacji (obszar_rys, &policja, 0); KlatkaAnimacji (obszar_rys, &pilka1, 2); KlatkaAnimacji (obszar_rys, &auto1, 3); /* --- Trzeba uaktualnić cały ekran --- */
Duszki i animacja
391
uakt_prostokat.x = 0; uakt_prostokat.y = 0; uakt_prostokat.width = obszar_rys->allocation.width; uakt_prostokat.height = obszar_rys->allocation.height; /* --- Uaktualniamy go --- */ gtk_widget_draw (obszar_rys, &uakt_prostokat); } /* * configure_event * * Tworzy nową drugoplanową piksmapę o odpowiednich * rozmiarach. Wywoływana jest przy każdej zmianie * rozmiarów okna. Musimy zwolnić przydzielone * zasoby. */ static gint configure_event (GtkWidget *kontrolka, GdkEventConfigure *zdarzenie) { /* --- Zwalniamy drugoplanową piksmapę, jeśli już ją przydzieliliśmy --- */ if (piksmapa) { gdk_pixmap_unref (piksmapa); } /* --- Tworzymy piksmapę o nowych rozmiarach --- */ piksmapa = gdk_pixmap_new (kontrolka->window, kontrolka->allocation.width, kontrolka->allocation.height, -1); return TRUE; } /* * expose_event * * Wywoływana wtedy, kiedy zostanie odsłonięte * okno, albo po wywołaniu procedury gdk_widget_draw. * Kopiuje drugoplanową piksmapę do okna. */
Część III Rysowanie, kolor i GDK
392
gint expose_event (GtkWidget *kontrolka, GdkEventExpose *zdarzenie) { /* --- Kopiujemy piksmapę do okna --- */ gdk_draw_pixmap (kontrolka->window, kontrolka->style->fg_gc[GTK_WIDGET_STATE (kontrolka)], piksmapa, zdarzenie->area.x, zdarzenie->area.y, zdarzenie->area.x, zdarzenie->area.y, zdarzenie->area.width, zdarzenie->area.height); return FALSE; } /* * zamknij * * Koniec programu */ void zamknij () { gtk_exit (0); } /* * PobierzSzerIWys * * Pobiera szerokość i wysokość z danych xpm * */ void PobierzSzerIWys (gchar **xpm, int *szerok, int *wysok) { sscanf (xpm [0], "%d %d", szerok, wysok); } /* * main * * Program zaczyna się tutaj */ int main (int argc, char *argv[])
Duszki i animacja
{ GtkWidget *okno; GtkWidget *obszar_rys; GtkWidget *xpole; /* --- Inicjacja GTK --- */ gtk_init (&argc, &argv); if (argc > 1) { bUzyjMaski = FALSE; } /* --- Tworzymy okno najwyższego poziomu --- */ okno = gtk_window_new (GTK_WINDOW_TOPLEVEL); xpole = gtk_hbox_new (FALSE, 0); gtk_container_add (GTK_CONTAINER (okno), xpole); gtk_widget_show (xpole); gtk_signal_connect (GTK_OBJECT (okno), "destroy", GTK_SIGNAL_FUNC (zamknij), NULL); /* --- Tworzymy obszar rysunkowy --- */ obszar_rys = gtk_drawing_area_new (); gtk_drawing_area_size (GTK_DRAWING_AREA (obszar_rys), 200, 300); gtk_box_pack_start (GTK_BOX (xpole), obszar_rys, TRUE, TRUE, 0); gtk_widget_show (obszar_rys); /* --- Sygnały używane do obsługi drugoplanowej piksmapy --- */ gtk_signal_connect (GTK_OBJECT (obszar_rys), "expose_event", (GtkSignalFunc) expose_event, NULL); gtk_signal_connect (GTK_OBJECT(obszar_rys),"configure_event", (GtkSignalFunc) configure_event, NULL); /* --- Pokazujemy okno --- */ gtk_widget_show (okno); /* --- Przerysowujemy co pewien czas --- */ gtk_timeout_add (100, Przerysuj, obszar_rys); /* --- Ładujemy wszystkie duszki --- */ LadujPiksmapy (okno, &pan, duszek_pan);
393
Część III Rysowanie, kolor i GDK
394 LadujPiksmapy (okno, &rower, duszek_rower); LadujPiksmapy (okno, &pani, duszek_pani);
LadujPiksmapy (okno, &policja, duszek_policja); LadujPiksmapy (okno, &pilka1, duszek_pilka); LadujPiksmapy (okno, &auto1, duszek_auto); /* --- Rozmieszczamy duszki --- */ rower.x = 30; rower.y = 60; pan.x = 50; pan.y = 60; pan.x = 60; pan.y = 60; policja.x = 60; policja.y = 90; pilka1.x = 0; pilka1.y = 90; auto1.x = 0; auto1.y = 120; /* --- Wywołujemy główną pętlę GTK --- */ gtk_main (); return 0; }
Gry wideo Możliwości GDK wykraczają poza prostą animację. Możemy wykorzystać wiedzę zdobytą w poprzednim rozdziale, aby stworzyć ramy gry opartej na popularnym w latach osiemdziesiątych Defenderze. W grze tej gracz latał nad powierzchnią planety, usiłując powstrzymać obcych najeźdźców przed porwaniem swoich ludzi. Obcy próbowali uprowadzić ludzi z powierzchni i donieść ich na górę ekranu, gdzie ulegali mutacji. Mutacja powodowała śmierć człowieka i powstanie groźniejszego obcego. Autor musiał odtworzyć grę z pamięci (nie zdołał znaleźć salonu gier, w którym ciągle stałyby antyczne automaty). Konieczne było także wyeliminowanie pewnych cech oryginału (ze względu na ograniczoną objętość książki). Program nie
Duszki i animacja
395
nalicza punktów, brakuje także niektórych obcych. Celem jest zademonstrowanie możliwości GDK w krótkiej grze, którą łatwo można rozszerzyć (co, jak zwykle, pozostawiamy jako ćwiczenie dla czytelników).
Gry oparte na GTK+/GDK W przykładowym programie wykorzystamy wiedzę o duszkach i animacji, wyniesioną z lektury poprzedniego rozdziału, aby stworzyć szkielet gry. Animacja w Defenderze przypomina animację w poprzednim przykładzie, ale tym razem jednym z duszków steruje gracz, a komputer kontroluje pozostałe duszki oraz oddziaływania pomiędzy wszystkimi obiektami w grze. Jak łatwo się domyślić, możemy użyć GTK+ i GDK do stworzenia bardziej wypracowanych gier, niż Defender. Oryginalna gra jest dużo bardziej skomplikowania, ale poniższy przykład pokazuje, że w GTK+ można napisać grę wideo nie obciążającą zbytnio procesora, zwłaszcza na nowszym sprzęcie (innymi słowy, na Pentium 133 gra pracowała szybko, wykorzystując procesor w niewielkim stopniu. Prawdopodobnie nie działałaby zbyt dobrze na 386 albo 486SX-16).
Elementy gry Podczas tworzenia gry należy rozważyć kilka czynników: sterowanie, grafikę, sztuczną inteligencję, efekty specjalne, sprawdzanie kolizji, poziomy trudności oraz naliczanie punktów. Z pewnością chcielibyśmy umieścić te elementy w grze. W przykładowym programie nie będziemy jednak zajmować się wszystkimi tymi czynnikami; na przykład w naszej wersji Defendera nie będzie punktowania ani poziomów trudności. Oczywiście, w prawdziwej grze wzrastający poziom trudności jest niezbędny. Mało urozmaicona gra, która nie stanowi wyzwania, szybko kończy swój żywot na półce z przecenionym oprogramowaniem.
Wejście Klawiatura jest urządzeniem wejściowym, które na pewno posiadają wszyscy użytkownicy komputera, dlatego grą będziemy sterować za pośrednictwem klawiszy. W grze istnieje możliwość poruszania się (w czterech kierunkach) oraz oddawania strzałów do obcych (patrz rysunek 13.3). Gracz mógłby poruszać statkiem przy pomocy klawiszy kursora i strzelać klawiszem spacji, ale zaprogramowanie tego jest trudniejsze, niż można by przypuszczać. Pisanie gier różni się od pisania innych aplikacji-musimy w każdej chwili wiedzieć, które klawisze są wciśnięte. Gracz może na przykład przytrzymywać klawisz kierunku, jednocześnie uderzając spację, aby zestrzelić obce statki. W domyślnym trybie GTK+ nie da się tego przeprowadzić, ponieważ do aplikacji wysyłany jest tylko kod ostatnio naciśniętego klawisza. Jeśli użytkownik przytrzyma klawisz, wówczas
396
Część III Rysowanie, kolor i GDK
klawisz zacznie się powtarzać. Klawisze, które były wciśnięte przed naciśnięciem nowego klawisza, zostaną zignorowane.
Rysunek 13.3. Lądowniki porywają ludzi – jeden z lądowników eksplodował, upuszczając człowieka.
Sprawdzanie klawiszy Można uporać się z problemem klawiszy, używając sygnałów key_press_event i key_press_release. Po wciśnięciu klawisza wysyłany jest sygnał key_press_event, a po jego zwolnieniu – key_press_release. Mówiąc ściśle, sygnał key_press_release domyślnie nie jest wysyłany, więc zwykłe zdefiniowanie funkcji zwrotnej nie zadziała. Aby otrzymywać informacje o zwolnieniu klawiszy, musimy wywołać funkcję gtk_widget_set_events ze znacznikiem GDK_KEY_RELEASE_MASK. Dzięki temu sygnał key_release_ event będzie wysyłany do okna. GTK+ odfiltrowuje sygnały o mniejszym znaczeniu, więc musimy jawnie zażądać ich przekazywania.
Powtarzanie klawiszy Powtarzanie klawiszy jest w naszym programie zupełnie zbędne, chociaż jedynym efektem przytrzymywania wielu klawiszy byłoby niepotrzebne obciążenie, związane z przetwarzaniem nietypowych kombinacji klawiszy. Funkcja gdk_key_repeat_disable wyłącza powtarzanie klawiszy w aplikacji. Po wywołaniu tej funkcji każdy naciśnięty klawisz wysyła pojedynczy sygnał key_press_event, nawet jeśli zostanie przytrzymany. Funkcja gdk_key_repeat_disable ma jednak efekt globalny – wpływa nie tylko na grę, ale na wszystkie działające aplikacje. Aby temu zapobiec, funkcja gdk_key_repeat_disable jest wywoływana z procedury obsługi sygnału focus_in_event, który wskazuje, że gra otrzymała ognisko. Gdy ognisko opuści grę, funkcja zwrotna dla sygnału focus_out_event wywołuje funkcję gdk_key_repeat_restore, aby przywrócić powtarzanie klawiszy; dzięki temu
Duszki i animacja
397
gracz może spokojnie pracować w arkuszu kalkulacyjnym, kiedy do pokoju wejdzie szef.
Grafika Gra posiada kilka elementów graficznych: ekran radaru, który pokazuje wszystkie jednostki w grze, główny ekran, w którym rozgrywa się większość wydarzeń, duszki dla każdej jednostki oraz kilka efektów specjalnych. Każda jednostka jest piksmapą, tworzoną z danych xpm umieszczonych w kodzie aplikacji. Gra jest podwójnie buforowana, dzięki czemu animacja jest nienaganna. W każdej klatce gry przerysowywany jest cały ekran. Najpierw czyścimy drugoplanową piksmapę, następnie rysujemy na niej aktualnie widoczne jednostki, wreszcie rysujemy radar, który pokazuje rozmieszczenie wszystkich obiektów. Kiedy rysowanie dobiegnie końca, cały rysunek jest kopiowany z drugoplanowej piksmapy do kontrolki obszaru rysunkowego. Rysunki na głównym ekranie są piksmapami, natomiast radar jest tworzony z kolorowych punktów. Każdy kolor reprezentuje inną jednostkę, dzięki czemu po odrobinie treningu łatwo jest rozróżnić poszczególne obiekty. Radar pokazuje nawet pociski, wystrzeliwane przez obcych oraz eksplozje.
Sztuczna inteligencja (AI) Każda jednostka w grze posiada pewne zadania. Sztuczna inteligencja jest dość prymitywna, ale może stanowić wyzwanie – zwłaszcza, jeśli dodamy więcej wrogich jednostek. Przegrana byłaby wówczas dość prawdopodobna, gdyby w programie nie wyłączono sprawdzania kolizji gracza. Gra zawiera tylko dwie jednostki obdarzone sztuczną inteligencją, które dążą do osiągnięcia specyficznych celów. Lądowniki, które porywają ludzi, poruszają się poziomo, dopóki kogoś nie odszukają. Kiedy znajdą się bezpośrednio nad człowiekiem, wówczas opuszczają się w dół i próbują wynieść człowieka na górę ekranu, gdzie mogą się zamienić w mutantów (patrz rysunek 13.4). Mutanci polują na gracza-poruszają się w sposób do pewnego stopnia losowy, ale powoli zmierzają w stronę gracza.
398
Część III Rysowanie, kolor i GDK
Rysunek 13.4. Gracz strzela do mutantów – jeden został zabity, a drugi zginie za chwilę.
Sztuczna inteligencja mutanta powoduje jego ruch w osi x na jeden z czterech sposobów. Mutant może odsunąć się od gracza, pozostać w aktualnej pozycji, przysunąć się do gracza albo wykonać długi skok w stronę gracza. W połowie przypadków mutant będzie poruszał się w stronę gracza. Może czasem polecieć w przeciwnym kierunku, ponieważ jego ruch jest do pewnego stopnia chaotyczny, ale w dłuższym przedziale czasu będzie zbliżał się do gracza. Ruch w osi y jest podobny, ale zachodzi tylko wtedy, kiedy gracz jest w polu widzenia mutanta; w przeciwnym razie ruch ten jest przypadkowy. Ruchy jednostek są określane przez funkcję ModulAI. Każda wroga jednostka może oddać strzał do gracza, jeśli ten znajduje się w jej zasięgu. Mutanci mają naturalnie większą szansę oddania strzału.
Wewnętrzne struktury gry Program musi przechowywać stan wszystkich jednostek oraz ich położenie w świecie gry. Najważniejszymi elementami są duszki oraz góry. Duszki poruszają się nieprzerwanie, a góry są nieruchome, chociaż również zdają się poruszać, kiedy gracz leci do przodu. Ich pozycja (względem gracza) zmienia się, więc góry rysowane na ekranie również zmieniają się w trakcie poziomego lotu gracza. Ponieważ świat jest „zawinięty”, kod obliczający odległości i kierunki pomiędzy jednostkami jest dość skomplikowany. Aby uprościć program, wszystkie jednostki używają tej samej struktury danych, choć nie wszystkie wykorzystują każde jej pole.
Struktury danych Dwoma podstawowymi strukturami są typDuszek i typJednostka. Struktura typDuszek zawiera piksmapę duszka, razem z maską, wysokością i szerokością. Struktury typJednostka zawierają informacje o wszystkich jednostkach w grze. Gracz, obcy, pociski, a nawet eksplozje są opisane przez strukturę typJednostka. Przechowuje ona typ jednostki (LADOWNIK, MUTANT, POCISK, WYBUCH itd.), współrzędne jednostki, prędkość, czas życia oraz prawdopodobieństwo oddania strzału do gracza. Prawdopodobnie winni jesteśmy kilka wyjaśnień, dotyczących czasu życia poszczególnych jednostek. Czas życia większości jednostek jest nieskończony, o ile nie zostaną zestrzelone, ale niektóre obiekty istnieją tylko przez określony czas. Na przykład wystrzelone przez obcych pociski po chwili znikają, a więc musimy przypisać im czas życia, który ulega zmniejszeniu, kiedy pocisk się porusza. Kiedy czas życia osiągnie zero, pocisk zniknie z ekranu. Wybuchy przypominają pod tym względem pociski – tak naprawdę składają się z ośmiu małych duszków, rozrzucanych w kilku kierunkach i obdarzonych skończonym czasem życia. Wszystkie jednostki,
Duszki i animacja
399
z wyjątkiem gracza, są przechowywane na łączonej liście (GList *). Lista ta zawiera więc obcych, pociski oraz wszystkie osiem fragmentów każdego wybuchu.
Góry Góry są generowane po to, aby widać było ruch gracza, kiedy na ekranie nie ma żadnych obcych ani ludzi. Ich rola sprowadza się więc do punktu odniesienia, który pomaga zasymulować poruszanie się gracza. Generacja gór polega na utworzeniu losowo rozmieszczonych szczytów (x, y), które następnie używane są do stworzenia listy punktów, opisujących rozmieszczenie wzniesień i dolin.
Obliczenia Ponieważ gra rozgrywa się w wirtualnym świecie, rozciągającym się od 0 do X_ZAKRES i z powrotem do 0, podczas obliczania odległości i kierunków konieczne jest przeprowadzenie pewnych dodatkowych kroków. Załóżmy, że X_ZAKRES wynosi 2000. Jeśli współrzędna x gracza jest równa 1999, a wrogiej jednostki 1, wówczas odległość pomiędzy nimi wynosi nie 1998, ale 2, ponieważ świat zawija się od 1999 z powrotem do 0.
Typy danych Teraz, kiedy wyjaśniliśmy już, co powinien robić program, przyjrzyjmy się jego kodowi. Najpierw opiszemy typy danych i wykorzystywaną grafikę, a następnie zamieścimy kod implementujący grę.
defender.h #include /* * Kierunki ruchu gracza */ enum { RUCH_LEWO, RUCH_PRAWO, RUCH_GORA, RUCH_DOL }; /* * Bohaterowie gry */ enum {
Część III Rysowanie, kolor i GDK
400 GRACZ,
/* --- gracz --- */
OSOBA,
/* --- ludzie na powierzchni --- */
LADOWNIK,
/* --- obcy próbujący uprowadzić ludzi --- */
MUTANT,
/* --- obcy, którzy wynieśli ludzi na górę
POCISK,
/* --- obcy strzelający do gracza --- */
LASER,
/* --- gracz strzelający do obcych --- */
WYBUCH
/* --- ktoś zrobił "bum" --- */
ekranu i ulegli mutacji --- */
}; /* * Struktura danych używana przez wszystkie jednostki w grze */ typedef struct jednostka { int bZniszcz;
/* --- Skazany na zagładę --- */
int kierunek;
/* --- Dokąd zmierza jednostka? --- */
int typ;
/* --- Typ jednostki --- */
float pctStrzal;
/* --- Prawdopodobieństwo oddania strzału --- */
float x;
/* --- Położenie --- */
float y;
/* --- Położenie --- */
float vx;
/* --- Prędkość --- */
float vy;
/* --- Prędkość --- */
int zycie;
/* --- Pozostałe życia --- */
struct jednostka *przylacz;
/* --- Przyłączone jednostki --- */
} typJednostka; typedef struct { int x; int y; } typPunkt; typedef struct { typPunkt poczatek; typPunkt szczyt; typPunkt koniec; } typGora;
Duszki i animacja
401
/* * Duszki, przy pomocy których rysujemy jednostki na ekranie. */ typedef struct { char **dane_xpm;
/* --- Pierwotne dane xpm --- */
GdkPixmap *piksmapa;
/* --- Piksmapa --- */
GdkBitmap *maska;
/* --- Maska piksmapy --- */
int wysok;
/* --- Wysosokość duszka --- */
int szerok;
/* --- Szerokość duszka --- */
} typDuszek;
mutant.h Plik mutant.h zawiera rysunki wszystkich jednostek występujących w grze. /* * Rysunki wszystkich jednostek * występujących w Defenderze */ static char * xpm_pocisk[] = { "2 2 2 1", "
c None",
"X c #ffffff", "XX", "XX", }; static char * xpm_mutant[] = { "16 12 4 1", "
c None",
"b c #8888ff", "r c #FF3366", "G c #00AA00", " "
bbbr rrbbbrrr
", ",
" Gr r b b b r r r G ", " GGr rr rGG ", " GGr rr rGG ",
Część III Rysowanie, kolor i GDK
402 " GGr r r r r r r r GG ", " GGr r r r r r GG ", "
GGGrrGGG ",
"
GG rr GG ",
" GG rr GG ", " GG rr GG ", " GG rr
GG",
}; static char * xpm_ladownik[] = { "16 12 4 1", "
c None",
"y c #ffaa00", "g c #88FF88", "G c #009900", " "
yyyy
",
GGyyyyGG ",
" GGGGg g GGGG ", " GGG g g GGG ", " GGG g g GGG ", " GGGg g g g g g GGG ", " GGGg g g g GGG ", "
GGGGGGGG ",
"
GG GG GG ",
" GG GG GG ", " GG GG GG ", " GG GG GG ", }; static char * xpm_statek1 [] = { "22 7 4 1", " c None", "x c #777777", "p c #ff66ff", "o c #cccccc", " x " xxx " xxxxx
", ", ",
"xxxxxxxxxxxxxxpp
",
"xxxxxxxxxxxxxxxxxxxoo ",
Duszki i animacja
403
"ppppppxxooxxxxxxxxxooo", " pppppxxoo
"
}; static char * xpm_statek2 [] = { "22 7 4 1", " c None", "x c #777777", "p c #ff66ff", "o c #cccccc", "
x
"
xxx ",
" "
",
xxxxx ", ppxxxxxxxxxxxxxx",
" ooxxxxxxxxxxxxxxxxxxx", "oooxxxxxxxxxooxxpppppp", "
ooxxppppp ",
}; static char * xpm_osoba[] = { "6 10 4 1", [357] "
c None",
"y
c #ffaa00",
"p
c #CC00CC",
"P
c #FF44FF",
" pp ", " yP ", " yPPP ", " PPPP ", " PPPP ", " PPPP ", " pp ", " pp ", " pp ", " pp ", };
Część III Rysowanie, kolor i GDK
404 animacja.c
Kod w pliku animacja.c nie zawiera żadnych algorytmów używanych przez grę. Zajmuje się tworzeniem okien i przetwarzaniem zdarzeń. Kryjąca się za nim logika mogłaby być częścią dowolnej gry; akurat w tym przypadku mamy do czynienia z klonem Defendera. /* * Autor: Eric Harlow * Plik: animacja.c * * Tworzenie aplikacji w Linuksie * * Gra "Defender" */ #include #include #include "defender.h" /* --- Drugoplanowa piksmapa dla obszaru rysunkowego --- */ GdkPixmap *piksmapa = NULL; /* * WyswietlDuszka * * Wyświetla duszka na obszarze rysunkowym w punkcie o * określonych współrzędnych. Używamy maski, aby nie * rysować niewidocznego obszaru - powinien pozostać * przezroczysty. * * obszar_rys - gdzie należy narysować duszka * duszek - duszek do narysowania * x, y - pozycja rysowania duszka */ void WyswietlDuszka (GtkWidget *obszar_rys, typDuszek *duszek, int x, int y) { GdkGC *gc; /* --- Pobieramy kontekst gc zwykłego stylu --- */ gc = obszar_rys->style->fg_gc[GTK_STATE_NORMAL]; /* --- Ustawiamy maskę przycinania i punkt początkowy --- */
Duszki i animacja
gdk_gc_set_clip_mask (gc, duszek->maska); gdk_gc_set_clip_origin (gc, x, y); /* --- Kopiujemy piksmapę do drugoplanowej piksmapy --- */ gdk_draw_pixmap (piksmapa, obszar_rys->style->fg_gc[GTK_STATE_NORMAL], duszek->piksmapa, 0, 0, x, y, duszek->szerok, duszek->wysok); /* --- Zerujemy niepotrzebną już maskę przycinania. --- */ gdk_gc_set_clip_mask (gc, NULL); } /* * Przerysuj * * dane - kontrolka do przerysowania */ gint Przerysuj (gpointer dane) { GtkWidget* obszar_rys = (GtkWidget *) dane; GdkRectangle uakt_prostokat; /* --- Rysujemy okno gry na drugim planie --- */ RysujEkran (piksmapa, obszar_rys); /* --- Kopiujemy drugoplanową piksmapę na ekran --- */ uakt_prostokat.x = 0; uakt_prostokat.y = 0; uakt_prostokat.width = obszar_rys->allocation.width; uakt_prostokat.height = obszar_rys->allocation.height; gtk_widget_draw (obszar_rys, &uakt_prostokat); return (TRUE); } /* * configure_event * * Tworzy nową drugoplanową piksmapę o odpowiednich
405
Część III Rysowanie, kolor i GDK
406
* rozmiarach. Wywoływana przy każdej zmianie rozmiarów * okna. Musimy zwolnić przydzielone zasoby. */ static gint configure_event (GtkWidget *kontrolka, GdkEventConfigure *zdarzenie) { /* --- Zwalniamy drugoplanową piksmapę, jeśli już ją utworzyliśmy --- */ if (piksmapa) { gdk_pixmap_unref (piksmapa); } /* --- Tworzymy piksmapę o nowych rozmiarach --- */ piksmapa = gdk_pixmap_new (kontrolka->window, kontrolka->allocation.width, kontrolka->allocation.height, -1); return TRUE; } /* * OtrzymanoOgnisko * * Wywoływana wtedy, kiedy okno gry otrzyma ognisko. Jest * potrzebna, ponieważ klawisze powtarzają się i blokują inne * klawisze wciśnięte w tym samym momencie. Jedynym sposobem * uniknięcia tego efektu jest wyłączenie powtarzania klawiszy * i samodzielna obsługa zdarzeń "wciśnięty" i "zwolniony". * Zmiana ta jesr globalna: dotyczy wszystkich aplikacji, * więc powinniśmy dokonywać jej tylko wtedy, kiedy okno * gry otrzyma ognisko. */ static gint OtrzymanoOgnisko (GtkWidget *kontrolka, gpointer dane) { gdk_key_repeat_disable (); return (FALSE); } /* * UtraconoOgnisko
Duszki i animacja
407
* * Patrz OtrzymanoOgnisko. Ponieważ zmiana jest globalna, * powinniśmy przywrócić powtarzanie klawiszy, aby inne * aplikacje działały poprawnie. */ static gint UtraconoOgnisko (GtkWidget *kontrolka, gpointer dane) { gdk_key_repeat_restore (); return (FALSE); } /* * NacisniecieKlawisza * * Użytkownik nacisnął klawisz. Dodajemy go do listy aktualnie * wciśniętych klawiszy. */ static gint NacisniecieKlawisza (GtkWidget *kontrolka, GdkEventKey *zdarzenie) { DodajKlawisz (zdarzenie); return (FALSE); } /* * ZwolnienieKlawisza * * Użytkownik zwolnił klawisz. Usuwamy go z listy aktualnie * wciśniętych klawiszy. */ static gint ZwolnienieKlawisza (GtkWidget *kontrolka, GdkEventKey *zdarzenie) { UsunKlawisz (zdarzenie); return (FALSE); } /* * expose_event * * Wywoływana po odsłonięciu okna albo po wywołaniu * procedury gdk_widget_draw. Kopuje drugoplanową piksmapę
Część III Rysowanie, kolor i GDK
408 * do okna. */
gint expose_event (GtkWidget *kontrolka, GdkEventExpose *zdarzenie) { /* --- Kopiujemy piksmapę do okna --- */ gdk_draw_pixmap (kontrolka->window, kontrolka->style->fg_gc[GTK_WIDGET_STATE (kontrolka)], piksmapa, zdarzenie->area.x, zdarzenie->area.y, zdarzenie->area.x, zdarzenie->area.y, zdarzenie->area.width, zdarzenie->area.height); return FALSE; } /* * PobierzPioro * * Zwraca pióro utworzone na podstawie przekazanego * koloru GdkColor. "Pióro" (po prostu GdkGC) jest * tworzone i zwracane, gotowe do użycia. */ GdkGC *PobierzPioro (GdkColor *c) { GdkGC *gc; /* --- Tworzymy kontekst gc --- */ gc = gdk_gc_new (piksmapa); /* --- Ustawiamy pierwszy plan na określony kolor --- */ gdk_gc_set_foreground (gc, c); /* --- Zwracamy pióro --- */ return (gc); } /* * NowyKolor * * Tworzymy i przydzielamy GdkColor na podstawie * określonych parametrów.
Duszki i animacja
*/ GdkColor *NowyKolor (long czerwona, long zielona, long niebieska) { /* --- Przydzielamy miejsce na kolor --- */ GdkColor *c = (GdkColor *) g_malloc (sizeof (GdkColor)); /* --- Wypełniamy składowe koloru --- */ c->red = czerwona; c->green = zielona; c->blue = niebieska; gdk_color_alloc (gdk_colormap_get_system (), c); return (c); } /* * zamknij * * Kończymy aplikację */ void zamknij () { gtk_exit (0); } /* * main * * Program zaczyna się tutaj. */ int main (int argc, char *argv[]) { GtkWidget *okno; GtkWidget *obszar_rys; GtkWidget *xpole; ZacznijGre (); gtk_init (&argc, &argv); okno = gtk_window_new (GTK_WINDOW_TOPLEVEL);
409
Część III Rysowanie, kolor i GDK
410 xpole = gtk_hbox_new (FALSE, 0);
gtk_container_add (GTK_CONTAINER (okno), xpole); gtk_widget_show (xpole); gtk_signal_connect (GTK_OBJECT (okno), "destroy", GTK_SIGNAL_FUNC (zamknij), NULL); /* --- Tworzymy obszar rysunkowy --- */ obszar_rys = gtk_drawing_area_new (); gtk_drawing_area_size (GTK_DRAWING_AREA (obszar_rys), 400, PobierzWysokoscOkna ()); gtk_box_pack_start (GTK_BOX (xpole), obszar_rys, TRUE, TRUE, 0); gtk_widget_show (obszar_rys); gtk_widget_set_events (okno, GDK_KEY_RELEASE_MASK); /* --- Sygnały używane do obsługi drugoplanowej piksmapy --- */ gtk_signal_connect (GTK_OBJECT (obszar_rys), "expose_event", (GtkSignalFunc) expose_event, NULL); gtk_signal_connect (GTK_OBJECT (obszar_rys), "configure_event", (GtkSignalFunc) configure_event, NULL); /* --- Sygnały do obsługi ogniska --- */ gtk_signal_connect (GTK_OBJECT (okno), "focus_in_event", (GtkSignalFunc) OtrzymanoOgnisko, NULL); gtk_signal_connect (GTK_OBJECT (okno), "focus_out_event", (GtkSignalFunc) UtraconoOgnisko, NULL); /* --- Sygnały naciśnięcia klawisza --- */ gtk_signal_connect (GTK_OBJECT (okno), "key_press_event", (GtkSignalFunc) NacisniecieKlawisza, NULL); gtk_signal_connect (GTK_OBJECT (okno), "key_release_event", (GtkSignalFunc) ZwolnienieKlawisza, NULL); /* --- Pokazujemy okno --- */ gtk_widget_show (okno); /* --- Przerysowujemy co 1/20 sekundy --- */ gtk_timeout_add (50, Przerysuj, obszar_rys); LadujRysunki (okno);
Duszki i animacja
411
/* --- Wywołujemy główną pętlę gtk --- */ gtk_main (); return 0; } /* * PobierzSzerIWys * * Pobiera wysokość i szerokość piksmapy z danych xpm. */ void PobierzSzerIWys (gchar **xpm, int *szerok, int *wysok) { sscanf (xpm [0], "%d %d", szerok, wysok); }
klawisze.c Plik klawisze.c zajmuje się obsługą wciśniętych klawiszy i wywołuje odpowiednie funkcje poruszające statek i oddające strzały, w zależności od naciśniętego klawisza. /* * Autor: Eric Harlow * Plik: klawisze.c * Tworzenie aplikacji GUI w Linuksie */ #include #include #include "defender.h" #include "prototypy.h" /* * Ruch */ int klawiszLewo = 0; int klawiszPrawo = 0; int klawiszGora = 0; int klawiszDol = 0; /*
Część III Rysowanie, kolor i GDK
412 * Strzelanie */ int klawiszStrzal = 0; /* * DodajKlawisz * * Dodajemy klawisz do listy klawiszy, których
* naciskanie będziemy sprawdzać. Sprawdzamy tylko * klika klawiszy, ignorując całą resztę. */ void DodajKlawisz (GdkEventKey *zdarzenie) { switch (zdarzenie->keyval) { /* --- Strzałka w lewo --- */ case GDK_Left: klawiszLewo = TRUE; break; /* --- Strzałka w prawo --- */ case GDK_Right: klawiszPrawo = TRUE; break; /* --- Strzałka w górę --- */ case GDK_Up: klawiszGora = TRUE; break; /* --- Strzałka w dół --- */ case GDK_Down: klawiszDol = TRUE; break; /* --- Spacja --- */ case ' ': klawiszStrzal = TRUE; break; /* --- Ignorujemy resztę --- */
Duszki i animacja
default: break; } } /* * UsunKlawisz * * Jeśli sprawdzany klawisz zostanie zwolniony, * zerujemy znacznik, który wskazuje, że jest * naciśnięty. */ void UsunKlawisz(GdkEventKey *zdarzenie) { switch (zdarzenie->keyval) { case GDK_Left: klawiszLewo = FALSE; break; case GDK_Right: klawiszPrawo = FALSE; break; case GDK_Up: klawiszGora = FALSE; break; case GDK_Down: klawiszDol = FALSE; break; case ' ': klawiszStrzal = FALSE; break; default: break; } } /*
413
Część III Rysowanie, kolor i GDK
414 * ObsluzWcisnieteKlawisze * * Kiedy należy przesunąć wszystkie jednostki, * procedura ta jest wywoływana, aby poruszyć * gracza/oddać strzał w zależności od * aktualnie wciśniętych klawiszy. */ void ObsluzWcisnieteKlawisze () { /* * Przesuwamy statek w różnych kierunkach */ if (klawiszLewo) { RuchGracza (RUCH_LEWO); } if (klawiszPrawo) { RuchGracza (RUCH_PRAWO); } if (klawiszGora) { RuchGracza (RUCH_GORA); } if (klawiszDol) { RuchGracza (RUCH_DOL); } /* * Strzelamy *
* Po oddaniu strzału zerujemy wskaźnik wciśnięcia * klawisza, aby gracz musiał naciskać spację po * każdym strzale. W przeciwnym przypadku mógłby * po prostu przytrzymać klawisz. */ if (klawiszStrzal) { /* --- Strzał! --- */ StrzalGracza (); /* --- Jedno wciśnięcie klawisza = jeden strzał --- */
Duszki i animacja
415
klawiszStrzal = FALSE; } }
defender.c Plik ten jest głównym modułem, zawierającym logikę gry i funkcje rysujące. Nie zajmuje się interfejsem użytkownika, tylko kreśleniem obszaru rysunkowego, który tworzy pole gry. /* * Plik: defender.c * Autor: Eric Harlow * * przypomina grę Defender z lat osiemdziesiątych * (choć nie jest to pełna wersja) * */ #include #include "defender.h" #include "prototypy.h" #include "mutant.h" #include /* * Ilu ma być ludzi i obcych na początku gry? */ #define POCZ_OBCY 10 #define POCZ_LUDZIE 10 /* * Poniżej znajdują się stałe używane przy obliczaniu * przyspieszenia gracza. Rezultatem tarcia jest * zwolnienie ruchu. MAKS_PRED oznacza maksymalną * prędkość, jaką może osiągnąć gracz. */ #define TARCIE .5 #define MAKS_PREDK 16 #define PRZYSPIESZENIE 3.5
416
Część III Rysowanie, kolor i GDK
/* * Strzały z lasera również można konfigurować. Promień * lasera ma prędkość (szybkość przemieszczania się) oraz * długość (efekt głównie wizualny). */ #define DLUG_LASERA 60 #define PREDK_LASERA 60 /* * Zakres pola gry */ #define ZAKRES_X 2000 /* * Jak wysokie są szczyty gór? */ #define MAKS_SZCZYT 150 /* * Liczba generowanych szczytów górskich */ #define LICZBA_SZCZYTOW 10 /* * --- Agresywność * * Mutanty są dużo agresywniejsze od lądowników. */ #define LADOWNIK_AGRESJA 3 #define MUTANT_AGRESJA 6 /* * W trakcie eksplozji korzystamy z ośmiu duszków, * rozchodzących się gwiaździście z miejsca wybuchu. * Poniżej znajduje się tych osiem kierunków. */ int x_wyb[] = {1, 1, 0, -1, -1, -1, 0, 1}; int y_wyb[] = {0, 1, 1, 1, 0, -1, -1, -1}; /* * Jak duży jest ekran radaru? */
Duszki i animacja
#define SZEROK_RADARU 200 #define WYSOK_RADARU 50 /* * Rozmiary pozostałych elementów ekranu */ #define WYSOK_OKNA 220 #define WYSOK_SPODU 30 #define WYSOK_OSOBY (WYSOK_RADARU+WYSOK_OKNA+7) /* * Kolory używane do rysowanie */ GdkGC *pioroZielone = NULL; GdkGC *pioroBiale = NULL; GdkGC *pioroFioletowe = NULL; GdkGC *pioroCzerwone = NULL; /* * Zmienne używane podczas obliczeń */ int nRegulacjaStatku; int nRegulacjaWzgledna; int nSzerokOkna; /* * Definiujemy duszki bohaterów gry */ typDuszek duszek_osoba[1] = { { xpm_osoba, NULL, NULL, 0, 0 } }; typDuszek duszek_statek1[1] = { { xpm_statek1, NULL, NULL, 0, 0 } }; typDuszek duszek_statek2[1] = { { xpm_statek2, NULL, NULL, 0, 0 } }; typDuszek duszek_ladownik[1] = { { xpm_ladownik, NULL, NULL, 0, 0 } }; typDuszek duszek_mutant[1] = { { xpm_mutant, NULL, NULL, 0, 0 } }; typDuszek duszek_pocisk[1] = { { xpm_pocisk, NULL, NULL, 0, 0 } }; /* * Statek może być skierowany w lewo lub w prawo (duszek_statek1, * duszek_statek2) - ale do rysowania wykorzystujemy wskaźnik * duszek_statek. Jeśli statek zmieni kierunek lotu, należy * odpowiednio zmodyfikować wskaźnik. */ typDuszek *duszek_statek = duszek_statek1;
417
Część III Rysowanie, kolor i GDK
418 /* * Lista szczytów górskich. */ GList *listaTerenu = NULL; /* * Wszystkie jednostki w grze. */ GList *listaJednostek = NULL; /* * Gracz */ typJednostka *gracz = NULL; /* * Prototypy */
GdkColor *NowyKolor (long czerwona, long zielona, long niebieska); GdkGC *PobierzPioro (GdkColor *c); void PobierzSzeriWys (gchar **xpm, int *szerok, int *wysok); void WyswietlDuszka (GtkWidget *obszar_rys, typDuszek *duszek, int x, int y); /* * JednostkaX * * Przekształca współrzędną X jednostki na współrzędną * ekranową, liczoną względem położenia gracza. */ int JednostkaX (typJednostka *jednostka) { int xPoz; /* --- Regulujemy -x- jeśli przekroczy zakres --- */ if (jednostka->x < 0) { jednostka->x += ZAKRES_X; } else if (jednostka->x > ZAKRES_X) { jednostka->x -= ZAKRES_X; } /* --- Relatywizujemy współrzędną --- */
Duszki i animacja
xPoz = (int) (jednostka->x - nRegulacjaWzgledna); /* --- Ponownie regulujemy -x- jeśli przekroczy zakres --- */ if (xPoz < 0) xPoz += ZAKRES_X; if (xPoz > ZAKRES_X) xPoz -= ZAKRES_X; return (xPoz); } /* * EkranX * * Pobieramy współrzędną -x- w grze i przekształcamy ją * we współrzędną -x- na ekranie. */ int EkranX (int x) { int xPoz; /* --- Regulujemy -x- jeśli przekroczy zakres --- */ if (x < 0) { x += ZAKRES_X; } else if (x > ZAKRES_X) { x -= ZAKRES_X; } /* --- Współrzędna -x- jest absolutna --- */ xPoz = (int) (x - nRegulacjaWzgledna); /* --- Ponownie regulujemy -x- jeśli przekroczy zakres --- */ if (xPoz < (-(ZAKRES_X - nSzerokOkna) / 2)) { xPoz += ZAKRES_X; } else if (xPoz > ((ZAKRES_X - nSzerokOkna) / 2)) { xPoz -= ZAKRES_X; } return (xPoz);
419
Część III Rysowanie, kolor i GDK
420 } /* * GraX * * Pobieramy współrzędną -x- na ekranie i * przekształcamy ją na współrzędną -x- w grze. */ int GraX (int x) { int xPoz; /* --- Współrzędna względem gracza --- */ xPoz = (int) (x + nRegulacjaWzgledna); /* --- Upewniamy się, że nie przekroczyliśmy zakresu --- */ if (xPoz < 0) xPoz += ZAKRES_X; if (xPoz > ZAKRES_X) xPoz -= ZAKRES_X; return (xPoz); } /* * Ruch *
* Przesuwa jednostkę w kierunku x o vx (prędkość w osi x) i * w kierunku y o vy. */ void Ruch (typJednostka *jednostka) { /* --- Przesuwamy jednostkę --- */ jednostka->y += jednostka->vy; jednostka->x += jednostka->vx; /* --- ...ale utrzymujemy ją w granicach pola gry --- */ if (jednostka->x < 0) jednostka->x += ZAKRES_X; if (jednostka->x > ZAKRES_X) jednostka->x -= ZAKRES_X; } /* * LadujPiksmapy
Duszki i animacja
421
* * Ładuje rysunki do duszków. */ void LadujPiksmapy (GtkWidget *kontrolka, typDuszek *duszki) { /* --- Tworzymy piksmapę z danych xpm --- */ duszki->piksmapa = gdk_pixmap_create_from_xpm_d ( kontrolka->window, &duszki->maska, NULL, duszki->dane_xpm); /* --- Pobieramy szerokość i wysokość --- */ PobierzSzerIWys (duszki->dane_xpm, &duszki->szerok, &duszki->wysok); } /* * LadujRysunki * * Ładuje rysunki, aby umożliwić wyświetlenie ich w oknie * gry i skonfigurowanie kolorów. */ void LadujRysunki (GtkWidget *okno) { /* --- Ładujemy rysunki --- */ LadujPiksmapy (okno, duszek_osoba); LadujPiksmapy (okno, duszek_statek1); LadujPiksmapy (okno, duszek_statek2); LadujPiksmapy (okno, duszek_ladownik); LadujPiksmapy (okno, duszek_mutant); LadujPiksmapy (okno, duszek_pocisk); /* --- Pobieramy zdefiniowane kolory --- */ pioroCzerwone = PobierzPioro (NowyKolor (0xffff, 0x8888, 0x8888)); pioroZielone = PobierzPioro (NowyKolor (0, 0xffff, 0)); pioroFioletowe = PobierzPioro (NowyKolor (0xffff, 0, 0xffff)); pioroBiale = PobierzPioro (NowyKolor (0xffff, 0xffff, 0xffff));
Część III Rysowanie, kolor i GDK
422 } /* * UtworzGracza * * Tworzymy strukturę typJednostka dla gracza i * inicjujemy jej wartości. */ typJednostka *UtworzGracza () { /* --- Przydzielamy pamięć --- */ gracz = g_malloc (sizeof (typJednostka)); /* --- Inicjujemy dane gracza --- */ gracz->bZniszcz = FALSE; gracz->kierunek = 1; gracz->typ = GRACZ; gracz->x = 0; gracz->y = 150; gracz->vx = 0; gracz->vy = 0; gracz->przylacz = NULL; /* --- Zwracamy obiekt --- */ return (gracz); } /* * StrzalGracza *
* Gracz otworzył ogień! Tworzymy strzał laserowy * i dodajemy go do globalnej listy jednostek. */ void StrzalGracza () { typJednostka *laser; /* --- Tworzymy laser --- */ laser = (typJednostka *) g_malloc (sizeof (typJednostka)); /* --- Kierunek jest taki sam, jak kierunek ruchu statku --- */ laser->kierunek = gracz->kierunek;
Duszki i animacja
laser->typ = LASER; /* * Umieszczamy początkową pozycję lasera przed dziobem * statku. */ if (laser->kierunek > 0) { laser->x = gracz->x + (duszek_statek->szerok / 2); } else { laser->x = gracz->x - (duszek_statek->szerok / 2); } laser->y = gracz->y + 4; laser->vx = PREDK_LASERA * gracz->kierunek; laser->vy = 0; laser->przylacz = NULL; laser->bZniszcz = 0; /* --- Promień laser istnieje przez dwa ruchy --- */ laser->zycie = 2; /* --- Dodajemy laser do listy jednostek --- */ listaJednostek = g_list_append (listaJednostek, laser); } /* * RuchGracza * * Przesuwa gracza w określonym kierunku. */ void RuchGracza (int kierunek) { switch (kierunek) { case RUCH_GORA: gracz->y -= 3; break; case RUCH_DOL: gracz->y += 3; break;
423
Część III Rysowanie, kolor i GDK
424 case RUCH_LEWO: /* * Upewniamy się, że dziób statku jest * zwrócony we właściwym kierunku. */ duszek_statek = duszek_statek2; gracz->kierunek = -1; /* --- Przyspieszamy statek --- */ gracz->vx -= PRZYSPIESZENIE; break; case RUCH_PRAWO: /* * Upewniamy się, że dziób statku jest * zwrócony we właściwym kierunku. */ duszek_statek = duszek_statek1; gracz->kierunek = 1; /* --- Przyspieszamy statek --- */ gracz->vx += PRZYSPIESZENIE; break; } } /* * DodajTarcie * * Zwalniamy stopniowo statek i upewniamy się, że
* gracz nie przekroczy prędkości światła, trzymając * wciśnięty klawisz. */ void DodajTarcie () { /* --- Zwalniamy statek --- */ if (gracz->vx > TARCIE) { gracz->vx -= TARCIE; } else if (gracz->vx < -TARCIE) { gracz->vx += TARCIE; } else {
Duszki i animacja
/* --- Prędkość mniejsza od tarcia, zatrzymujemy statek --- */ gracz->vx = 0; } /* --- Nie pozwalamy na przekroczenie maksymalnej prędkości --- */ if (gracz->vx > MAKS_PREDK) gracz->vx = MAKS_PREDK; if (gracz->vx < -MAKS_PREDK) gracz->vx = -MAKS_PREDK; } /* * UtworzOsobe * * Tworzymy ludzi na planecie. */ typJednostka *UtworzOsobe () { typJednostka *osoba; /* --- Przydzielamy pamięć --- */ osoba = g_malloc (sizeof (typJednostka)); /* --- Inicjujemy osobę --- */ osoba->bZniszcz = FALSE; osoba->kierunek = 0; osoba->typ = OSOBA; osoba->x = rand () % ZAKRES_X; osoba->y = WYSOK_OSOBY; osoba->vx = 0; osoba->vy = 0; osoba->przylacz = NULL; return (osoba); } /* * RozmiescLudzi * * Tworzy osoby i rozmieszcza je losowo na ekranie */ void RozmiescLudzi ()
425
Część III Rysowanie, kolor i GDK
426 { int i; typJednostka *osoba; /* --- Tworzymy wszystkich ludzi --- */ for (i = 0; i < POCZ_LUDZIE; i++) { /* --- Tworzymy osobę --- */ osoba = UtworzOsobe (); /* --- Dodajemy ją do listy jednostek --- */
listaJednostek = g_list_append (listaJednostek, osoba); } } /* * UtworzObcego * * Tworzy obcy lądownik */ typJednostka *UtworzObcego () { typJednostka *obcy; /* --- Przydzielamy pamięć --- */ obcy = g_malloc (sizeof (typJednostka)); /* --- Inicjujemy strukturę --- */ obcy->bZniszcz = FALSE; obcy->pctStrzal = LADOWNIK_AGRESJA; obcy->typ = LADOWNIK; obcy->x = rand () % ZAKRES_X; obcy->y = rand () % 50 + WYSOK_RADARU; obcy->vx = 0; obcy->vy = 0; obcy->przylacz = NULL; return (obcy); } /* * UtworzPocisk
Duszki i animacja
427
* * Obcy strzelają pociskami. Pociski posiadają stały * kierunek ruchu i znikają po upływie pewnego czasu. */ typJednostka *UtworzPocisk (typJednostka *obcy, typJednostka *gracz) { float fdlug; typJednostka *pocisk; /* --- Przydzielamy pamięć --- */ pocisk = (typJednostka *) g_malloc (sizeof (typJednostka)); /* --- Inicjujemy strukturę --- */ pocisk->bZniszcz = FALSE; pocisk->pctStrzal = 0; pocisk->typ = POCISK; pocisk->x = obcy->x; pocisk->y = obcy->y; /* --- Obliczamy prędkość pocisku --- */ pocisk->vx = (float) OdlegloscPomiedzy (pocisk, gracz) * Kierunek (pocisk, gracz); pocisk->vy = (float) (gracz->y - obcy->y); /* * Regulujemy prędkość pocisku */ fdlug = sqrt (pocisk->vx * pocisk->vx + pocisk->vy * pocisk->vy); if (fdlug < .1) fdlug = .1; fdlug /= 3; pocisk->vx /= fdlug; pocisk->vy /= fdlug; pocisk->przylacz = NULL; /* * --- Pocisk posiada czas życia. Kiedy czas życia spadnie *
do zera, pocisk znika.
*/ pocisk->zycie = 60; return (pocisk);
Część III Rysowanie, kolor i GDK
428 } /* * DodajWybuch *
* Zniszczono jednostkę - tworzymy wybuch w miejscu, gdzie * znajdowała się jednostka. */ void DodajWybuch (typJednostka *jednostka) { typJednostka *frag; int i; /* --- Tworzymy osiem fragmentów --- */ for (i = 0; i < 8; i++) { /* --- Tworzymy fragment wybuchu --- */ frag = (typJednostka *) g_malloc (sizeof (typJednostka)); /* --- Inicjujemy fragment --- */ frag->bZniszcz = FALSE; frag->pctStrzal = 0; frag->typ = WYBUCH; frag->x = jednostka->x; frag->y = jednostka->y; /* --- Nadajemy mu sporą prędkość... --- */ frag->vx = x_wyb[i] * 5; frag->vy = y_wyb[i] * 5; frag->przylacz = NULL; /* --- ...i krótki czas życia --- */ frag->zycie = 20; /* --- Dodajemy go do listy jednostek --- */ listaJednostek = g_list_append (listaJednostek, frag); } } /* * RozmiescObcych *
Duszki i animacja
* Tworzymy obcych */ void RozmiescObcych () { int i; typJednostka *obcy; /* --- Tworzymy wszystkich obcych --- */ for (i = 0; i < POCZ_OBCY; i++) { /* --- Tworzymy jednego obcego --- */ obcy = UtworzObcego (); /* --- Dodajemy go do listy jednostek --- */ listaJednostek = g_list_append (listaJednostek, obcy); } } /* * OdlegloscPomiedzy * * Oblicza odległość pomiędzy dwoma jednostkami, korzystając * tylko ze współrzędnych x. Odległość nie jest po prostu * różnicą pomiędzy współrzędnymi x jednostek - ponieważ * świat jest zawinięty, musimy uwzględnić fakt, że * odległość pomiędzy 1 i ZAKRES_X to nie (ZAKRES_X-1) - 1. */ int OdlegloscPomiedzy (typJednostka *u1, typJednostka *u2) { int nOdleglosc; int nOdleglosc2; /* --- Która współrzędna jest większa? --- */ if (u1->x < u2->x) { /* --- Obliczamy odległość w obie strony --- */ nOdleglosc = u2->x - u1->x; nOdleglosc2 = ZAKRES_X + u1->x - u2->x; } else { /* --- Obliczamy odległość w obie strony --- */ nOdleglosc = u1->x - u2->x;
429
Część III Rysowanie, kolor i GDK
430 nOdleglosc2 = ZAKRES_X + u2->x - u1->x; } /* --- Wybieramy mniejszą odległość --- */
return ((nOdleglosc < nOdleglosc2) ? nOdleglosc : nOdleglosc2); } /* * Kierunek * * Jaki jest kierunek -x-, w którym powinna poruszać się * jednostka, aby zbliżyć się do innej jednostki? * Może to być -1, 1 albo 0. */ int Kierunek (typJednostka *u1, typJednostka *u2) { int nOdleglosc; int nOdleglosc2; if (u1->x < u2->x) { /* --- Odległość w obu kierunkach --- */ nOdleglosc = u2->x - u1->x; nOdleglosc2 = ZAKRES_X + u1->x - u2->x; } else { /* --- Odległość w obu kierunkach --- */ nOdleglosc2 = u1->x - u2->x; nOdleglosc = ZAKRES_X + u2->x - u1->x; } /* --- Kierunek zależy od mniejszej odległości --- */ return ((nOdleglosc < nOdleglosc2) ? 1 : -1); } /* * ProbaStrzalu * * Obcy strzela do gracza. */ void ProbaStrzalu (typJednostka *jednostka) {
Duszki i animacja
typJednostka *pocisk; /* --- Czy obcy jest dostatecznie blisko, aby oddać strzał? --- */ if (OdlegloscPomiedzy (gracz, jednostka) < (nSzerokOkna / 2)) { /* --- Losowe prawdopodobieństwo, określone przez odpowiedni parametr jednostki --- */ if ((rand () % 100) < jednostka->pctStrzal) { /* --- Tworzymy pocisk zmierzający w stronę gracza --- */ pocisk = UtworzPocisk (jednostka, gracz); /* --- Dodajemy go do listy jednostek --- */ listaJednostek = g_list_append (listaJednostek, pocisk); } } } /* * ModulAI * * Zawiera algorytmy ruchu wszystkich jednostek. Niektóre * jednostki (lądowniki) szukają ludzi, żeby unieść ich * w górę. Mutanci polują na gracza. Pociski po prostu * lecą, dopóki nie upłynie czas ich życia itd. */ void ModulAI (typJednostka *jednostka) { typJednostka *tmcz; int najlepszaOdl = 50000; typJednostka *najblizsza = NULL; GList *wezel; /* * Algorytm AI obcego lądownika */ if (jednostka->typ == LADOWNIK) { /* --- Jeśli obcy porwał człowieka... --- */ if (jednostka->przylacz) { /* --- Przesuwa się do góry --- */
431
Część III Rysowanie, kolor i GDK
432 najblizsza = jednostka->przylacz; jednostka->y -= .5; najblizsza->y -= .5; /* --- Jeśli dotarł na górę ekranu --- */
if (jednostka->y - (duszek_ladownik[0].wysok / 2) < WYSOK_RADARU) { /* --- Stapia się z człowiekiem --- */ jednostka->y = WYSOK_RADARU + (duszek_ladownik[0].wysok / 2); jednostka->typ = MUTANT; jednostka->pctStrzal = MUTANT_AGRESJA; jednostka->przylacz = NULL; najblizsza->bZniszcz = TRUE; } return; } /* --- Czy w pobliżu są ludzie do uprowadzenia? --- */ for (wezel = listaJednostek; wezel; wezel = wezel->next) { tmcz = (typJednostka *) wezel->data; if (tmcz->typ == OSOBA && tmcz->przylacz == NULL) { /* --- Szukamy najbliższej osoby --- */ if (OdlegloscPomiedzy (jednostka, tmcz) < najlepszaOdl) { najblizsza = tmcz; najlepszaOdl = OdlegloscPomiedzy (jednostka, tmcz); } } } /* --- Znaleźliśmy cel --- */ if (najlepszaOdl vx = 0; jednostka->x = najblizsza->x;
Duszki i animacja
/* * --- Sprawdzamy, czy można się połączyć --*/ if ((jednostka->y + (duszek_ladownik[0].wysok / 2) + .8) < (najblizsza->y - (duszek_osoba[0].wysok / 2))) { /* --- Obniżamy się. --- */ jednostka->y += .5; } else if ((jednostka->y + (duszek_ladownik[0].wysok / 2)) > (najblizsza->y - (duszek_osoba[0].wysok / 2))) { jednostka->y -= .5; } else { /* --- Porywamy człowieka --- */ jednostka->przylacz = najblizsza; najblizsza->przylacz = jednostka; najblizsza->zycie = 20; } /* --- Czy jest jakiś człowiek w rozsądnej odległości? --- */ } else if (najlepszaOdl < 20) { /* --- Lecimy w jego kierunku --- */ jednostka->vx = Kierunek (jednostka, najblizsza); jednostka->x += jednostka->vx; } else { /* * --- Nie ma ludzi w pobliżu. Poruszamy się losowo. */ if (jednostka->vx == 0) { if ((rand () % 2) == 0) { jednostka->vx = 1; } else { jednostka->vx = -1; } } jednostka->x += jednostka->vx; }
433
Część III Rysowanie, kolor i GDK
434 /* * Sprawdzamy, czy warto oddać strzał. */ ProbaStrzalu (jednostka); /* * Algorytm AI mutanta */ } else if (jednostka->typ == MUTANT) { /*
* --- Mutanci poruszają się losowo, ale zmierzają *
z wolna w kierunku gracza.
*/ jednostka->vx = Kierunek (jednostka, gracz) * ((rand () % 4) + 1); jednostka->vy = rand () % 5 - 2; /* * Jeśli gracz jest w zasięgu, zmierzamy w jego stronę * w osi -y-. */ if (OdlegloscPomiedzy (jednostka, gracz) < 200) { if (jednostka->y < gracz->y) jednostka->vy++; if (jednostka->y > gracz->y) jednostka->vy--; } /* --- Przesuwamy jednostkę --- */ Ruch (jednostka); /* --- Próbujemy strzelić --- */ ProbaStrzalu (jednostka); /* * --- Pociski i wybuchy */ } else if ((jednostka->typ == POCISK) || (jednostka->typ == WYBUCH)) { /* --- Jednostki te mają określony czas życia Zmniejszamy go. --- */ jednostka->zycie --;
Duszki i animacja
/* --- Przesuwamy jednostkę. --- */ Ruch (jednostka); /* --- Kiedy upłynie czas życia, niszczymy je --- */ if (jednostka->zycie bZniszcz = TRUE; } /* * Alogorytm AI osoby. */ } else if (jednostka->typ == OSOBA) { /* * Osoba porusza się samodzielnie tylko wtedy, kiedy * spada na ziemię, ponieważ został zestrzelony * niosący ją obcy. */ if (jednostka->przylacz==NULL && jednostka->y < WYSOK_OSOBY) { /* --- Przesuwamy osobę w dół --- */ jednostka->y += 2; } } } /* * PobierzDuszka * * Zwraca duszka danej jednostki */ typDuszek *PobierzDuszka (typJednostka *jednostka) { typDuszek *duszek; /* --- Pobieramy duszka --- */ switch (jednostka->typ) { case GRACZ: duszek = duszek_statek; break;
435
Część III Rysowanie, kolor i GDK
436 case OSOBA: duszek = duszek_osoba; break; case LADOWNIK: duszek = duszek_ladownik; break; case MUTANT: duszek = duszek_mutant; break; case POCISK: case WYBUCH: duszek = duszek_pocisk; break; default: duszek = NULL; break; } return (duszek); } /* * KtosPomiedzy * * Czy jakaś jednostka znajduje się pomiędzy tymi
* współrzędnymi? Funkcja ta używana jest podczas * obliczania, czy ktoś został trafiony. */ typJednostka *KtosPomiedzy (int x1, int y1, int x2, int y2) { GList *wezel; typJednostka *jednostka; typJednostka *najblizszaJednostka = NULL; int najblizszaX; typDuszek *duszek; int ekranX; /* --- Sprawdzamy wszystkie jednostki --- */ for (wezel = listaJednostek; wezel; wezel = wezel->next) {
Duszki i animacja
437
jednostka = (typJednostka *) wezel->data; /* --- Jeśli jest to czarny charakter --- */ if ((jednostka->typ == LADOWNIK) || (jednostka->typ == OSOBA) || (jednostka->typ == MUTANT)) { /* --- Pobieramy duszka i położenie na ekranie --- */ ekranX = JednostkaX (jednostka); duszek = PobierzDuszka (jednostka); /* --- Jeśli mamy duszka --- */ if (duszek) { /* --- Jeśli jest w określonym zakresie -x- --- */ if ((ekranX >= x1 && ekranX y - (duszek->wysok / 2) < y1) && jednostka->y + (duszek->wysok / 2) > y1) { /* * Nie znaleźliśmy jednostki, albo ta jest * bliżej. */ if ((najblizszaJednostka == NULL) || (abs (x1 - ekranX) < abs (x1 - najblizszaX))) { /* --- Ta jednostka jest najbliższa spośród sprawdzonych do tej pory --- */ najblizszaJednostka = jednostka; najblizszaX = ekranX; } } } } } }
Część III Rysowanie, kolor i GDK
438 return (najblizszaJednostka); } /* * JednostkaGora *
* Oblicza maksymalny pułap jednostki na podstawie * ekranu radaru i wysokości duszka. */ int JednostkaGora (typJednostka *jednostka) { typDuszek *duszek; /* --- Pobieramy duszka --- */ duszek = PobierzDuszka (jednostka); /* --- Dodajemy pół wysokości duszka do wysokości ekranu radaru --- */ return (WYSOK_RADARU + (duszek[0].wysok / 2)); } /* * JednostkaDol * * Obliczamy minimalny pułap jednostki na ekranie, * częściowo na podstawie rozmiarów jednostki. */ int JednostkaDol (typJednostka *jednostka) { typDuszek *duszek; duszek = PobierzDuszka (jednostka); return (PobierzWysokoscOkna () - (duszek[0].wysok / 2)); } /* * */ void RegulujWysokDuszka (typJednostka *jednostka) { typDuszek *duszek;
Duszki i animacja
int nGora; int nDol; /* --- Pobieramy duszka jednostki --- */ duszek = PobierzDuszka (jednostka); if (duszek == NULL) return; /* --- Obliczamy dolny i górny pułap jednostki --- */ nGora = JednostkaGora (jednostka); nDol = JednostkaDol (jednostka); /* --- Nie pozwalamy jej na przemieszczenie się zbyt nisko lub zbyt wysoko --- */ if (jednostka->y < nGora) { jednostka->y = nGora; } else if (jednostka->y > nDol) { jednostka->y = nDol; } } /* * RysujInneJednostki * * Wyświetla wszystkie jednostki na ekranie. Najpierw * musimy przesunąć wszystkie jednostki w nowe położenia. * Część tego zadania wykonuje moduł AI. * */ void RysujInneJednostki (GdkPixmap *piksmapa, GtkWidget *obszar_rys) { typJednostka *jednostka; typJednostka *jednostkaTrafiona; GList *wezel; int xPoz; int xPozKoniec; typDuszek *duszek; /* --- Dla każdej jednostki na liście --- */ for (wezel = listaJednostek; wezel; wezel = wezel->next) { /* --- Pobieramy jednostkę --- */ jednostka = (typJednostka *) wezel->data;
439
Część III Rysowanie, kolor i GDK
440 /*
* --- Wywołujemy moduł AI, aby ją przesunąć --*/ ModulAI (jednostka); /* * Jeśli jednostka została zniszczona przez moduł * AI, nie rysujemy jej. */ if (jednostka->bZniszcz) { continue; } /* * Jeśli jednostka nie ma duszka, nie możemy * jej teraz narysować. */ duszek = PobierzDuszka (jednostka); if (duszek == NULL) continue; /* --- W jakim kierunku zmierza jednostka? --- */ xPoz = JednostkaX (jednostka); /* --- Upewniamy się, że jednostka nie ucieknie poza pole gry --- */ RegulujWysokDuszka (jednostka); /* --- Wreszcie rysujemy jednostkę --- */ WyswietlDuszka (obszar_rys, duszek, (int) (xPoz - duszek[0].szerok / 2), (int) (jednostka->y - duszek[0].wysok / 2)); } /* * --- Kiedy wszystko jest już narysowanie, odpalamy laser */ for (wezel = listaJednostek; wezel; wezel = wezel->next) { jednostka = (typJednostka *) wezel->data; /* --- Jeśli jest to laser --- */
Duszki i animacja
441
if (jednostka->typ == LASER) { /* --- Pobieramy położenie początkowe i końcowe --- */ xPoz = EkranX ((int) jednostka->x); xPozKoniec = xPoz + DLUG_LASERA * jednostka->kierunek; /* --- Sprawdzamy, czy w coś trafiliśmy --- */ jednostkaTrafiona = KtosPomiedzy ((int) xPoz, (int) jednostka->y, (int) xPozKoniec, (int) jednostka->y); if (jednostkaTrafiona) { /* --- Trafiliśmy w coś --- */ /* --- Laser porusza się tylko dotąd --- */ xPozKoniec = JednostkaX (jednostkaTrafiona); /* --- Niszczymy jednostkę --- */ jednostkaTrafiona->bZniszcz = TRUE; jednostka->bZniszcz = TRUE; /* --- Efekty specjalne zniszczenia --- */ DodajWybuch (jednostkaTrafiona); } /* --- Rysujemy laser --- */ gdk_draw_line (piksmapa, pioroBiale, xPoz, jednostka->y, xPozKoniec, jednostka->y); /* --- Pobieramy rzeczywiste współrzędne lasera --- */ jednostka->x = GraX (xPozKoniec); /* --- Jeśli laser jest za daleko... --- */ if (OdlegloscPomiedzy (jednostka, gracz) > nSzerokOkna / 2) { /* --- ...niszczymy go --- */ jednostka->bZniszcz = TRUE; } } } }
Część III Rysowanie, kolor i GDK
442 /* * Funkcje obsługujące teren gry * */ /* * PorownajGory *
* Funkcja porównująca dla sortowania szczytów górskich */ gint PorownajGory (typGora *m1, typGora *m2) { return (m1->poczatek.x - m2->poczatek.x); } /* * DodajGore * * Dodaje szczyt górski do listy przechowującej wszystkie * szczyty. */ GList *DodajGore (GList *listaGor, int xszczyt, int yszczyt) { typGora *wezel; wezel = (typGora *) g_malloc (sizeof (typGora)); wezel->poczatek.x = xszczyt - yszczyt; wezel->poczatek.y = 0; wezel->szczyt.x = xszczyt; wezel->szczyt.y = yszczyt; wezel->koniec.x = xszczyt + yszczyt; wezel->koniec.y = 0; return (g_list_insert_sorted (listaGor, wezel, PorownajGory)); } /* * DodajPunkt * * Dodaje szczyt górski w (x, y) */
Duszki i animacja
GList *DodajPunkt (GList *listaTerenu, int x, int y) { typPunkt *p; /* --- Przydzielamy pamieć --- */ p = (typPunkt *) g_malloc (sizeof (typPunkt)); /* --- Inicjujemy punkt --- */ p->x = x; p->y = y; /* --- Dodajemy punkt do listy --- */ listaTerenu = g_list_append (listaTerenu, p); return (listaTerenu); } /* * GenerujTeren * * Tworzy losowo rozmieszczone szczyty górskie, które służą * do generowania terenu. */ void GenerujTeren () { int xszczyt; int yszczyt; GList *listaGor = NULL; GList *wezel; typGora *poprzGora; typGora *gora; int i; /* --- Obliczamy szczyty --- */ for (i = 0; i < LICZBA_SZCZYTOW; i++) { xszczyt = rand () % ZAKRES_X; yszczyt = rand () % MAKS_SZCZYT; listaGor = DodajGore (listaGor, xszczyt, yszczyt); } poprzGora = NULL;
443
Część III Rysowanie, kolor i GDK
444 listaTerenu = DodajPunkt (listaTerenu, 0, 0);
/* --- Obliczamy linie na podstawie listy szczytów --- */ for (wezel = listaGor; wezel; wezel = wezel->next) { gora = (typGora *) wezel->data; /* --- Pierwsza góra --- */ if (poprzGora == NULL) { listaTerenu = DodajPunkt (listaTerenu, gora->poczatek.x, gora->poczatek.y); listaTerenu = DodajPunkt (listaTerenu, gora->szczyt.x, gora->szczyt.y); poprzGora = gora; /* --- Linie nie mogą się krzyżować --- */ } else if (poprzGora->koniec.x < gora->poczatek.x) { listaTerenu = DodajPunkt (listaTerenu, poprzGora->koniec.x, poprzGora->koniec.y); listaTerenu = DodajPunkt (listaTerenu, gora->poczatek.x, gora->poczatek.y); listaTerenu = DodajPunkt (listaTerenu, gora->szczyt.x, gora->szczyt.y); poprzGora = gora; /* --- Poprzednia góra przykrywa obecną --- */ } else if (poprzGora->koniec.x > gora->koniec.x) { /* --- Na razie nic nie robimy --- */ } else { /* --- Góry się przecinają --- */ listaTerenu = DodajPunkt (listaTerenu, (poprzGora->koniec.x + gora->poczatek.x) / 2, (poprzGora->koniec.x - gora->poczatek.x) / 2); listaTerenu = DodajPunkt (listaTerenu, gora->szczyt.x, gora->szczyt.y); poprzGora = gora; } }
Duszki i animacja
445
listaTerenu = DodajPunkt (listaTerenu, poprzGora->koniec.x, poprzGora->koniec.y); listaTerenu = DodajPunkt (listaTerenu, ZAKRES_X, 0); } void ZacznijGre () { listaJednostek = NULL; /* --- Tworzymy statek gracza --- */ gracz = UtworzGracza (); /* --- Generujemy mapę --- */ GenerujTeren (); /* --- Rozmieszczamy ludzi --- */ RozmiescLudzi (); /* --- Rozmieszczamy obcych --- */ RozmiescObcych (); } /* * RysujZboczeGory * * Rysuje góry w tle gry. */ void RysujZboczeGory (GdkPixmap *piksmapa, typPunkt *ostatniPt, typPunkt *pt, int nGora, int nDol) { int x1; int x2; int nPoczatek; int nKoniec; int nWysok; nWysok = nDol - nGora; nPoczatek = gracz->x - (nSzerokOkna / 2); nKoniec = gracz->x + (nSzerokOkna / 2);
Część III Rysowanie, kolor i GDK
446 x1 = EkranX (ostatniPt->x); x2 = EkranX (pt->x); if ((x2 < 0) || (x1 > nSzerokOkna)) { /* --- Nic nie robimy --- */ } else { /* --- Rysujemy szczyt --- */ gdk_draw_line (piksmapa, pioroBiale, EkranX (ostatniPt->x),
(int) (nDol - ((ostatniPt->y * nWysok) / MAKS_SZCZYT)), EkranX (pt->x), (int) (nDol - ((pt->y * nWysok) / MAKS_SZCZYT))); } } void RysujGory (GdkPixmap *piksmapa, GtkWidget *obszar_rys, int nGora, int nDol) { typPunkt *ostatniPt; typPunkt *pt; GList *wezel; ostatniPt = NULL; /* --- Jaki jest punkt widzenia? --- */ for (wezel = listaTerenu; wezel; wezel = wezel->next) { /* --- Pobieramy punkt. --- */ pt = (typPunkt *) wezel->data; if (ostatniPt) { RysujZboczeGory (piksmapa, ostatniPt, pt, nGora, nDol); } ostatniPt = pt; } } void ObliczRegulacje (GtkWidget *obszar_rys)
Duszki i animacja
{ nRegulacjaStatku = (obszar_rys->allocation.width / 2); nRegulacjaWzgledna = gracz->x - nRegulacjaStatku; } /* * RysujWszystkieJednostki * * Rysuje wszystkie jednostki */ void RysujWszystkieJednostki (GdkPixmap *piksmapa, GtkWidget *obszar_rys) { /* * Przesuwamy i wyświetlamy gracza */ DodajTarcie (); Ruch (gracz); /* --- Utrzymujemy go w granicach pola gry --- */ RegulujWysokDuszka (gracz); WyswietlDuszka (obszar_rys, duszek_statek, nRegulacjaStatku - (duszek_statek->szerok / 2), (int) gracz->y - (duszek_statek->wysok / 2)); /* * Przesuwamy i wyświetlamy pozostałe jednostki. */ RysujInneJednostki (piksmapa, obszar_rys); } /* * RysujRadar * * Rysujemy wszystkie jednostki na ekranie radaru. Oczywiście, * najpierw należy narysować ekran radaru, a następnie * przekształcić rzeczywiste położenia jednostek na ich * położenia "radarowe". Rysujemy na ekranie małe punkty * w różnych kolorach, odzwierciedlających typ jednostki. */ void RysujRadar (GdkPixmap *piksmapa, GtkWidget *obszar_rys)
447
Część III Rysowanie, kolor i GDK
448 { int nPozostalo; GList *wezel; typJednostka *jednostka; int x, y; int nMin; int nowex, nowey; /* --- Pobieramy współrzędne radarowe --- */
nPozostalo = (obszar_rys->allocation.width - SZEROK_RADARU) / 2; nMin = gracz->x - (ZAKRES_X / 2); /* --- Czyścimy prostokąt --- */ gdk_draw_rectangle (piksmapa, obszar_rys->style->white_gc, FALSE, nPozostalo, 0, SZEROK_RADARU, WYSOK_RADARU); /* * --- Wyświetlamy wszystkie jednostki */ for (wezel = listaJednostek; wezel; wezel = wezel->next) { jednostka = (typJednostka *) wezel->data; x = jednostka->x; y = jednostka->y; if (x > gracz->x + (ZAKRES_X / 2)) { x -= ZAKRES_X; } else if (x < gracz->x - (ZAKRES_X / 2)) { x += ZAKRES_X; } /* --- Zamieniamy -x- na współrzędne radarowe --- */ nowex = (x - nMin) * SZEROK_RADARU / ZAKRES_X; /* --- Zamieniamy -y- na współrzędne radarowe --- */ nowey = ((y - WYSOK_RADARU) * (WYSOK_RADARU - 6) / WYSOK_OKNA) + 2; switch (jednostka->typ) {
Duszki i animacja
449
case OSOBA: gdk_draw_rectangle (piksmapa, pioroFioletowe, TRUE, nPozostalo + nowex-1, nowey-1, 2, 2); break; case LADOWNIK: gdk_draw_rectangle (piksmapa, pioroZielone, TRUE, nPozostalo + nowex-1, nowey-1, 2, 2); break; case MUTANT: gdk_draw_rectangle (piksmapa, pioroCzerwone, TRUE, nPozostalo + nowex-1, nowey-1, 2, 2); break; case POCISK: case WYBUCH: gdk_draw_rectangle (piksmapa, pioroBiale, TRUE, nPozostalo + nowex, nowey, 1, 1); break; } } /* * --- Umieszczamy na radarze także gracza. */ /* --- Zamieniamy -x- na współrzędne radarowe --- */ nowex = (gracz->x - nMin) * SZEROK_RADARU / ZAKRES_X; /* --- Zamieniamy -y- na współrzędne radarowe --- */ nowey = ((gracz->y - WYSOK_RADARU) * (WYSOK_RADARU - 6) / WYSOK_OKNA) + 2; gdk_draw_rectangle (piksmapa, pioroBiale, TRUE, nPozostalo + nowex-1, nowey-1, 2, 2); } /* * RysujEkran * * Procedura ta wykonuje bardzo wiele czynności. Rysuje tło * i wszystkie jednostki, a także radar. Jest to w istocie
Część III Rysowanie, kolor i GDK
450
* główna pętla gry, wywoływana co pewien czas, określony * przez częstotliwość czasomierza. */ void RysujEkran (GdkPixmap *piksmapa, GtkWidget *obszar_rys) { /* --- Przesuwamy gracza w zależności od klawiszy --- */ ObsluzWcisnieteKlawisze (); /* --- Pobieramy szerokość okna --- */ nSzerokOkna = obszar_rys->allocation.width; /* --- Obliczamy przesunięcie gracza --- */ ObliczRegulacje (obszar_rys); /* --- czyścimy piksmapę (drugoplanowy bufor) --- */ gdk_draw_rectangle (piksmapa, obszar_rys->style->black_gc, TRUE, 0, 0, obszar_rys->allocation.width, obszar_rys->allocation.height); /* --- Rysujemy górne obramowanie i ekran radaru --- */ gdk_draw_line (piksmapa, obszar_rys->style->white_gc, 0, WYSOK_RADARU, obszar_rys->allocation.width, WYSOK_RADARU); /* --- Wysokie góry... --- */ RysujGory (piksmapa, obszar_rys, obszar_rys->allocation.height - 65, obszar_rys->allocation.height - WYSOK_SPODU); /* --- Rysujemy jednostki --- */ RysujWszystkieJednostki (piksmapa, obszar_rys); /* --- Rysujemy jednostki na radarze --- */ RysujRadar (piksmapa, obszar_rys); /* --- Sprawdzamy kolizje --- */ SprawdzenieKolizji ();
Duszki i animacja
/* --- Usuwamy zniszczone jednostki --- */ ZwolnijZniszczoneJedn (); } /* * SprawdzenieKolizji * * Czy gracz zderzył się z jakąś jednostką? Sprawdzamy * kolizję, ale niczego nie robimy. W końcu nie jest * to prawdziwa gra, więc demonstrujemy tylko, jak * można to zrobić. */ void SprawdzenieKolizji () { GList *wezel; typDuszek *duszek; int gracz_x; int jednostka_x; typJednostka *jednostka; /* --- Sprawdzamy, czy gracz z czymś się zderzył --- */ for (wezel = listaJednostek; wezel; wezel = wezel->next) { /* --- Pobieramy jednostkę --- */ jednostka = (typJednostka *) wezel->data; /* --- Pobieramy duszka jednostki --- */ duszek = PobierzDuszka (jednostka); if (duszek == NULL) continue; /* --- Eliminujemy sytuacje, w których nie doszło do kolizji --- */ if (gracz->y + (duszek_statek->wysok / 2) < jednostka->y - (duszek->wysok / 2)) continue; if (gracz->y - (duszek_statek->wysok / 2) > jednostka->y + (duszek->wysok / 2)) continue; gracz_x = JednostkaX (gracz); jednostka_x = JednostkaX (jednostka);
451
Część III Rysowanie, kolor i GDK
452 if (gracz_x + (duszek_statek->szerok / 2)
szerok / 2)) continue; if (gracz_x - (duszek_statek->szerok / 2) > jednostka_x + (duszek->szerok / 2)) continue; /* --- Żaden z powyższych warunków nie jest spełniony, więc gracz z czymś się zderzył --- */ } } /* * PobierzWysokoscOkna * * Zwraca wysokość okna gry. */ int PobierzWysokoscOkna () { return (WYSOK_RADARU + WYSOK_OKNA + WYSOK_SPODU); } /* * ZwolnijZniszczoneJedn * * Jednostki są najpierw oznaczane jako zniszczone. Procedura * ta przegląda listę i usuwa wszystkie zaznaczone jednostki, * zwalniając zajmowaną przez nie pamięć. */ void ZwolnijZniszczoneJedn () { GList *wezel; GList *nastepny; typJednostka *jednostka; wezel = listaJednostek; while (wezel) { nastepny = wezel->next; jednostka = (typJednostka *) wezel->data; /* --- Jeśli jednostka ma zostać zniszczona --- */
Duszki i animacja
453
if (jednostka->bZniszcz) { /* --- Usuwamy ją z listy jednostek --- */ listaJednostek = g_list_remove (listaJednostek, jednostka); /* --- Jeśli jednostka była połączona z inną --- */ if (jednostka->przylacz) { /* --- Usuwamy połączenie z tamtej jednostki --- */ jednostka->przylacz->przylacz = NULL; } g_free (jednostka); } wezel = nastepny; } }
Podsumowanie GTK+ wraz z GDK można wykorzystać do tworzenia animacji i gier wideo. Wydajność GTK+ jest wystarczająca dla prostych gier, nawet na niezbyt szybkich komputerach. Bardziej złożone gry wideo wymagają szybszego sprzętu i prawdopodobnie szybszej biblioteki.
Rozdział 14 Drzewa, c-listy i zakładki Drzewa (GtkTree), c-listy (GtkCList) i zakładki (GtkNotebook) są bardziej interesujące, niż poprzednio omawiane proste kontrolki. Ilustrują wielkie możliwości drzemiące w GTK+. Udostępniając obiektowy interfejs, znacznie ułatwiają tworzenie aplikacji. Omawiane w tym rozdziale trzy kontrolki są jednymi z najbardziej przydatnych, choć GTK+ oferuje znacznie więcej kontrolek.
Kontrolka drzewa Kontrolka drzewa (GtkTree) wyświetla dane w postaci hierarchicznej; może to być drzewo katalogów albo drzewo genealogiczne. Drzewa umożliwiają oglądanie całej hierarchii elementów, ale pozwalają też na zwinięcie poszczególnych gałęzi drzewa. Rysunek 14.1 przedstawia przykład kontrolki GtkTree umieszczonej obok pola listy, co umożliwia przeglądanie katalogów i plików. W drzewie, po lewej stronie, wyświetlane są tylko katalogi. Podkatalogi wszystkich umieszczonych w drzewie katalogów są wyświetlane jako liście drzewa i można je zwinąć. Pole listy wyświetla pliki, znajdujące się w zaznaczonym katalogu. Napiszemy ten program w trakcie zapoznawania się z kontrolką drzewa.
Część IV Rozbudowa GTK+
458
Rysunek 14.1. Kontrolka drzewa w działaniu.
Tworzenie drzewa Do tworzenia kontrolki GtkTree służy funkcja gtk_tree_new. Utworzone drzewo jest początkowo puste i trzeba je wypełnić. Aby utworzyć liść drzewa, należy użyć funkcji gtk_tree_item_new_with_label i przekazać do niej nazwę liścia. Po utworzeniu liścia należy przypisać go do węzła macierzystego. Węzłem tym może być kontrolka GtkTree (dla węzła najwyższego poziomu) albo inny liść. Poniższy kod tworzy kontrolkę GtkTree i dodaje do niej węzeł najwyższego poziomu. /* --- Tworzymy drzewo --- */ drzewo = gtk_tree_new(); /* --- Tworzymy liść --- */ lisc = gtk_tree_item_new_with_label ("Pierwszy liść"); /* --- Dodajemy liść do drzewa --- */ gtk_tree_append (GTK_TREE (drzewo), lisc); /* --- uwidaczniamy drzewo i liść --- */ gtk_widget_show (lisc); gtk_widget_show (drzewo);
Teraz możemy zacząć dodawać kolejne liście, ale dodawanie danych tworzących drzewo odbywa się w kilku krokach. Najpierw tworzymy nowe drzewo GtkTree przy pomocy funkcji gtk_tree_new. Następnie za-
Drzewa, c-listy i zakładki
459
znaczamy je jako poddrzewo i przypisujemy mu drzewo macierzyste, przy pomocy funkcji gtk_tree_item_set_subtree. Możemy następnie dodawać elementy do poddrzewa za pomocą tych samych funkcji, które dodają elementy do drzewa. /* --- Tworzymy nowe drzewo --- */ poddrzewo = gtk_tree_new(); /* --- Liść będzie rodzicem poddrzewa --- */ gtk_tree_item_set_subtree (GTK_TREE_ITEM (lisc), poddrzewo); /* --- Tworzymy nowy liść --- */ nowy_lisc = gtk_tree_item_new_with_label ("liœæ"); /* --- Dodajemy nowy liść do poddrzewa --- */ gtk_tree_append (GTK_TREE (poddrzewo), nowy_lisc);
Funkcja gtk_tree_append nie jest jedyną funkcją, która dodaje elementy do drzewa. Możemy wykorzystać także funkcję gtk_tree_prepend, która wstawia elementy na początku drzewa, oraz funkcję gtk_tree_insert, która wstawia elementy w określonym punkcie drzewa. /* --- Dodajemy element na początku drzewa --- */ gtk_tree_prepend (GTK_TREE (drzewo), lisc); /* --- Wstawiamy element na początek przy pomocy "insert" --- */ gtk_tree_insert (GTK_TREE (drzewo), lisc, 0); /* --- Wstawiamy element na koniec przy pomocy "insert" --- */ gtk_tree_insert (GTK_TREE (drzewo), lisc, -1); /* --- Wstawiamy element za drugim elementem --- */ gtk_tree_insert (GTK_TREE (drzewo), lisc, 2);
Aby usunąć element drzewa, możemy skorzystać z funkcji gtk_tree_remove_item. Kiedy element zostanie usunięty z drzewa, usuwane są także jego wszystkie liście. /* --- Usuwamy liść z drzewa --- */ gtk_tree_remove_item (drzewo, lisc);
Aby oczyścić całe drzewo z liści, możemy użyć funkcji gtk_tree_clear_ items. Elementy, które mają zostać usunięte, określamy przy pomocy początkowej i końcowej pozycji. Wszystkie liście potomne można usunąć za pomocą polecenia: /* --- Usuwamy wszystkie liście drzewa --- */
Część IV Rozbudowa GTK+
460 gtk_tree_clear_items (drzewo, 0, -1);
Sygnały drzewa Kontrolka GtkTree dysponuje pewną liczbą własnych sygnałów. Sygnały selection_changed, select_child i unselect_child są wysyłane do kontrolki drzewa w celu poinformowania o stanie zaznaczonych elementów. Elementy drzewa mogą otrzymywać sygnały collapse_tree i expand_tree.
Tworzenie przeglądarki plików Wyposażeni w podane wyżej informacje możemy zacząć pisanie programu przeglądarki plików. Przeglądarka składa się z kontrolki GtkTree oraz pola listy, umieszczonych obok siebie wewnątrz okna aplikacji. Najpierw przedstawimy główną funkcję, która tworzy okno programu: /* * main * * Program zaczyna się tutaj */ int main (int argc, char *argv[]) { GtkWidget *okno; /* --- Inicjacja GTK --- */ gtk_init (&argc, &argv); /* --- Tworzymy okno najwyższego poziomu --- */ okno = gtk_window_new (GTK_WINDOW_TOPLEVEL); /* --- Nadajemy mu tytuł --- */ gtk_window_set_title (GTK_WINDOW (okno), "Pliki w drzewie"); /* --- Ustawiamy rozmiar okna. --- */ gtk_widget_set_usize (okno, 250, 250); /* --- Należy zawsze podłączyć sygnał delete_event * do głównego okna. */ gtk_signal_connect (GTK_OBJECT (okno), "delete_event", GTK_SIGNAL_FUNC (delete_event), NULL);
Drzewa, c-listy i zakładki
461
gtk_widget_show (okno); /* --- Tworzymy drzewo --- */ UtworzDrzewo (okno); gtk_main (); exit (0); }
Funkcja UtworzDrzewo tworzy poziome pole pakujące, w którym umieścimy obok siebie kontrolkę drzewa i pole listy. Kontrolka drzewa będzie umieszczona w przewijanym oknie; dzięki temu, jeśli elementy drzewa zostaną rozwinięte i drzewo przekroczy rozmiar okna, użytkownik będzie mógł skorzystać z pasków przewijania i zobaczyć resztę drzewa. Funkcja getcwd zwraca bieżący katalog roboczy, który zostanie wyświetlony w korzeniu kontrolki drzewa. Element ten pokaże użytkownikowi pełną ścieżkę do katalogu, w którym będzie umieszczony program. Informacje o podkatalogach są przekazywane do funkcji UtworzPoddrzewo, aby wypełnić kontrolkę drzewa wszystkimi katalogami niższego rzędu. /* * UtworzDrzewo * * Tworzy drzewo pokazujące strukturę plików. * * okno - okno macierzyste */ static void UtworzDrzewo (GtkWidget *okno) { char bufor[MAKS_SCIEZKA]; GtkWidget *pole1; GtkWidget *pole2; GtkWidget *okno_przew; GtkWidget *drzewo; GtkWidget *lisc; /* --- Poziome pole pakujące --- */ pole1 = gtk_hbox_new (FALSE, 0); gtk_container_add (GTK_CONTAINER (okno), pole1); gtk_widget_show (pole1); /* --- Pole pakujące na drzewo --- */ pole2 = gtk_vbox_new(FALSE, 0);
Część IV Rozbudowa GTK+
462
gtk_box_pack_start(GTK_BOX(pole1), pole2, TRUE, TRUE, 0); gtk_container_border_width(GTK_CONTAINER(pole2), 5); gtk_widget_show(pole2); /* --- Tworzymy przewijane okno na drzewo --- */ okno_przew = gtk_scrolled_window_new (NULL, NULL); gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (okno_przew), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); gtk_box_pack_start (GTK_BOX (pole2), okno_przew, TRUE, TRUE, 0); gtk_widget_set_usize (okno_przew, 250, 250); gtk_widget_show (okno_przew); /* * --- Tworzymy korzeń drzewa */ drzewo = gtk_tree_new(); /* * --- Tworzymy pole listy */ polelisty = gtk_list_new (); gtk_widget_set_usize (polelisty, 250, 250); gtk_box_pack_start (GTK_BOX (pole1), polelisty, TRUE, TRUE, 0); gtk_widget_show (polelisty); /* --- Dodajemy drzewo do przewijanego okna --- */ gtk_scrolled_window_add_with_viewport ( GTK_SCROLLED_WINDOW(okno_przew), drzewo); /* --- Uwidaczniamy drzewo. --- */ gtk_widget_show (drzewo); /* * --- Tworzymy kontrolkę dla podstawowego elementu * (bieżącego katalogu */ lisc = gtk_tree_item_new_with_label ( getcwd (bufor, sizeof (bufor))); /* --- Dodajemy element do drzewa --- */ gtk_tree_append (GTK_TREE (drzewo), lisc);
Drzewa, c-listy i zakładki
463
/* --- Uwidaczniamy element --- */ gtk_widget_show (lisc); /* --- Tworzymy poddrzewo tego elementy --- */ UtworzPoddrzewo (getcwd (bufor, sizeof (bufor)), getcwd (bufor, sizeof (bufor)), lisc); /* --- Uwidaczniamy okno --- */ gtk_widget_show (okno); }
Funkcja UtworzPoddrzewo pobiera katalogi, znajdujące się na ścieżce. Katalogi te stają się liśćmi węzła, który reprezentuje ścieżkę. Na przykład katalog ksiazka może mieć podkatalogi rozdz1, rozdz2 i rozdz3. W takim przypadku węzeł drzewa ksiazka będzie miał trzy węzły potomne— rozdz1, rozdz2 i rozdz3. Funkcja opendir pozwala na przejrzenie wszystkich plików i podkatalogów w dowolnym katalogu. Następnie wykorzystujemy funkcję Katalog, która sprawdza, czy dana pozycja katalogu jest zwykłym plikiem, czy też katalogiem. Jeśli element jest katalogiem, wówczas rekurencyjnie przekazujemy go do funkcji UtworzPoddrzewo, aby znaleźć jego węzły potomne. Każdy węzeł posiada funkcję zwrotną dla sygnału select, więc kiedy użytkownik kliknie na nazwie katalogu, możemy wyświetlić w polu listy wszystkie zawarte w katalogu pliki. /* * UtworzPoddrzewo * * Dodaje katalogi do drzewa i wiąże je z procedurą * obsługi sygnału - kiedy element zostanie wybrany, * pole listy zostanie wypełnione plikami znajdującymi * się w katalogu. * * szSciezka - Ścieżka do dodawanych plików. */ static void UtworzPoddrzewo (char *szSciezka, char *szKatalog, GtkWidget* element) { DIR *katalog; struct dirent *wpisKatalogowy; GtkWidget* poddrzewo_elementu = NULL; GtkWidget* nowy_element; char bufor[MAKS_SCIEZKA];
Część IV Rozbudowa GTK+
464 /* --- Odczytujemy bieżący katalog --- */ katalog = opendir (szSciezka); /* --- Odczytując zawartość katalogu... --- */ while (wpisKatalogowy = readdir (katalog)) {
/* --- ...nie bierzemy pod uwagę tych wpisów --- */ if (!strcmp (wpisKatalogowy->d_name, "..") || !strcmp (wpisKatalogowy->d_name, ".")) { /* --- Ignorujemy katalogi "." i ".." --- */ } else { /* --- Tworzymy pełną ścieżkę --- */ sprintf (bufor, "%s/%s", szSciezka, wpisKatalogowy->d_name); /* --- Jeśli jest to katalog --- */ if (Katalog (bufor)) { if (poddrzewo_elementu == NULL) { /* --- Tworzymy nowe poddrzewo --- */ poddrzewo_elementu = gtk_tree_new (); /* --- Dodajemy poddrzewo do drzewa --- */ gtk_tree_item_set_subtree (GTK_TREE_ITEM ( element), poddrzewo_elementu); } /* --- Tworzymy nowy element dla pliku --- */ nowy_element = gtk_tree_item_new_with_label ( wpisKatalogowy->d_name); /* --- Dodajemy element do drzewa --- */ gtk_tree_append (GTK_TREE (poddrzewo_elementu), nowy_element); /* --- Dodajemy wszystkie elementy podrzędne --- */ UtworzPoddrzewo (bufor, wpisKatalogowy->d_name, nowy_element); /* --- Uwidaczniamy element --- */ gtk_widget_show (nowy_element);
Drzewa, c-listy i zakładki
465
/* --- Informujemy o wybraniu elementu --- */ gtk_signal_connect (GTK_OBJECT (nowy_element), "select", GTK_SIGNAL_FUNC (wybrano_element), g_strdup (bufor)); } } } /* --- Gotowe --- */ closedir (katalog); gtk_widget_show (element); }
Poniżej zamieszczamy funkcję Katalog, która przyjmuje nazwę elementu katalogu i używa funkcji stat, aby sprawdzić, czy element ten jest plikiem, czy katalogiem. /* * Katalog * * Sprawdza, czy plik jest katalogiem, czy zwykłym plikiem. * * bufor - pełna ścieżka wraz z nazwą pliku. * * zwraca TRUE, jeśli plik jest katalogiem */ int Katalog (char *bufor) { struct stat buf; if (stat (bufor, &buf) < 0) { /* --- Błąd - ignorujemy. --- */ return (FALSE); } /* --- Sprawdzamy, czy plik jest katalogiem --- */ return (S_ISDIR (buf.st_mode)); }
Funkcja wybrano_element jest wywoływana za każdym razem, kiedy użytkownik kliknie element kontrolki drzewa. Funkcja wyświetla krótki
Część IV Rozbudowa GTK+
466
komunikat (w celach ilustracyjnych) i wywołuje funkcję WypelnijPoleListy, która wyświetla listę plików w katalogu. void wybrano_element (GtkWidget *kontrolka, gpointer dane) { g_print ("wybrano element %s\n", (char *) dane); WypelnijPoleListy ((char *) dane); }
Kod funkcji WypelnijPoleListy właściwie nie wymaga wyjaśnień. Otrzymawszy ścieżkę, funkcja przegląda wszystkie pliki/katalogi na ścieżce i wypełnia pole listy nazwami wszystkich plików, które znajdują się w katalogu. /* * WypelnijPoleListy * * Dodaje pliki, znajdujące się w katalogu do pola * listy. Uwzględnia tylko zwykłe pliki. * * szSciezka - ścieżka do dodawanych plików. */ static void WypelnijPoleListy (char *szSciezka) { DIR *katalog; struct dirent *wpisKatalogowy; char bufor[MAKS_SCIEZKA]; /* --- Czyścimy pole listy. --- */ gtk_list_clear_items (GTK_LIST (polelisty), 0, g_list_length (GTK_LIST (polelisty)->children)); /* --- Odczytujemy bieżący katalog --- */ katalog = opendir (szSciezka); /* --- Odczytując bieżący katalog... --- */ while (wpisKatalogowy = readdir (katalog)) { /* --- ...nie bierzemy pod uwagę tych wpisów --- */ if (!strcmp (wpisKatalogowy->d_name, "..") || !strcmp (wpisKatalogowy->d_name, ".")) { /* --- Ignorujemy katalogi "." i ".." --- */ } else {
Drzewa, c-listy i zakładki
467
/* --- Tworzymy pełną ścieżkę --- */ sprintf (bufor, "%s/%s", szSciezka, wpisKatalogowy->d_name); /* --- Jeśli to nie jest katalog --- */ if (!Katalog (bufor)) { /* --- Dodajemy plik do pola listy --- */ DodajElementListy (polelisty, wpisKatalogowy->d_name); } } } /* --- Gotowe --- */ closedir (katalog); }
Funkcja DodajElementListy jest po prostu skrótem, który dodaje łańcuch do pola listy. /* * DodajElementListy * * Dodaje element do pola listy. * * polelisty - lista, do której należy dodać element. * sTekst - tekst, który należy wyświetlić w polu listy. */ void DodajElementListy (GtkWidget *polelisty, char *sTekst) { GtkWidget *element; /* --- Tworzymy element listy na podstawie danych --- */ element = gtk_list_item_new_with_label (sTekst); /* --- Dodajemy element do pola listy --- */ gtk_container_add (GTK_CONTAINER (polelisty), element); /* --- Uwidaczniamy element --- */ gtk_widget_show (element); }
W rezultacie otrzymujemy aplikację przeglądarki plików, która używa bieżącego katalogu jako punktu startowego, odczytuje strukturę systemu
Część IV Rozbudowa GTK+
468
plików (nie sprawdzając żadnych katalogów położonych wyżej od bieżącego) i wyświetla drzewo systemu plików poniżej katalogu bieżącego. Kliknięcie na nazwie katalogu spowoduje wyświetlenie znajdujących się w nim plików. Poszczególne katalogi można rozwijać lub zwijać, klikając znak plusa albo minusa umieszczony obok nazwy katalogu.
Kontrolka notatnika Kontrolka notatnika (GtkNotebook) umożliwia wyświetlanie informacji na kilku stronach. Każdą ze stron można przesunąć na wierzch, klikając na reprezentującej ją zakładce. Zakładki są opisane, dzięki czemu można zidentyfikować poszczególne strony. Zakładki mogą znajdować się na górze strony, na dole strony, albo na jej prawej lub lewej krawędzi.
Rysunek 14.2. Kontrolka notatnika.
Kontrolkę GtkNotebook tworzymy przy pomocy funkcji gtk_notebook_new. Zakładki dodajemy przy pomocy funkcji gtk_notebook_set_tab_pos, przekazując jedno z możliwych położeń (GTK_POS_TOP, GTK_POS_BOTTOM, GTK_POS_LEFT albi GTK_POS_RIGHT). /* --- Tworzymy notatnik --- */ notatnik = gtk_notebook_new(); /* --- Umieszczamy zakładki na górze notatnika --- */
Drzewa, c-listy i zakładki
469
gtk_notebook_set_tab_pos (GTK_NOTEBOOK (notatnik), GTK_POS_TOP);
Dodawanie i usuwanie stron Kontrolkę GtkNotebook można używać dopiero wtedy, kiedy dodamy do niej kilka stron. Czynimy to przy pomocy funkcji gtk_notebook_append_ page, która dodaje nowe strony na końcu listy stron. Funkcja gtk_ notebook_prepend_page dodaje strony na początku listy, a gtk_notebook_ insert_page umożliwia wstawienie strony w dowolnym miejscu listy. Kontrolki przekazywane do tych funkcji muszą zostać utworzone przed próbą utworzenia strony. Zazwyczaj tworzy się etykietę dla zakładki oraz jakiś typ pojemnika, w którym będzie można umieścić inne kontrolki. Aby usunąć stronę, można użyć funkcji gtk_notebook_remove_page. /* --- Tworzymy etykietę i pojemnik dla nowej strony --- */ etykieta = gtk_label_new ("Pierwsza zakładka"); pojemnik = gtk_frame_new ("Informacje jawne"); /* --- Dodajemy stronę na końcu notatnika --- */ gtk_notebook_append_page (notatnik, pojemnik, etykieta); /* --- Tworzymy etykietę i pojemnik dla nowej strony --- */ etykieta = gtk_label_new ("Ostatnia zakładka"); pojemnik = gtk_frame_new ("Informacje ściśle tajne"); /* --- Dodajemy stronę na końcu notatnika --- */ gtk_notebook_append_page (notatnik, pojemnik, etykieta); /* --- Tworzymy etykietę i pojemnik dla nowej strony --- */ etykieta = gtk_label_new ("Środkowa zakładka"); pojemnik = gtk_frame_new ("Informacje niejawne"); /* --- Wstawiamy stronę w środek notatnika --- */ ***gtk_notebook_append_page (notatnik, pojemnik, etykieta);
Manipulowanie stronami Można użyć funkcji gtk_notebook_current_page, aby pobrać bieżącą stronę oraz funkcji gtk_notebook_set_page, aby ustawić bieżącą stronę. Funkcje gtk_notebook_next_page i gtk_notebook_prev_page umożliwiają poruszanie się w górę i w dół na liście stron. Funkcja gtk_notebook_set_show_tabs pozwala wyświetlić albo ukryć zakładki notatnika.
470
Część IV Rozbudowa GTK+
/* --- Pobieramy bieżącą stronę --- */ nStrona = gtk_notebook_current_page (notatnik); /* --- Przechodzimy do następnej strony --- */ gtk_notebook_set_page (notatnik, nStrona + 1); /* --- Przechodzimy do poprzedniej strony --- */ gtk_notebook_prev_page (notatnik); /* --- Przechodzimy do następnej strony (znowu) --- */ gtk_notebook_next_page (notatnik); /* --- Ukrywamy zakładki przed użytkownikiem --- */ gtk_notebook_set_show_tabs (notatnik, false);
Ukrywanie zakładek może początkowo wydać się dziwnym pomysłem, ponieważ użytkownik nie będzie mógł wówczas kliknąć na zakładce, aby przejść do innej strony notatnika, ale czasem aktualnie wyświetlana strona powinna być ustawiana przez aplikację. Przypomina to mechanizm znany z kreatorów w aplikacjach firmy Microsoft, które prowadzą użytkownika przez skomplikowaną procedurę. Procedura może na przykład wymagać przejścia przez dziesięć kroków, a zazwyczaj łatwiej jest wyświetlać jednocześnie tylko jeden krok. Kiedy użytkownik zakończy czynności w danym kroku, aplikacja może przejść do następnej strony i wykonać następny krok. Program znajdujący się na końcu tego rozdziału zawiera w swoim oknie kilka stron, które ilustrują wykorzystanie kontrolki GtkNotebook w aplikacji.
Kontrolka GtkCList Kontrolka GtkCList pozwala na wyświetlenie tabelarycznych danych wewnątrz pojedynczej kontrolki. Może zawierać dowolną liczbę kolumn i rzędów i pozwala na ustawienie szerokości kolumn na żądaną wartość. Kontrolkę tę często wykorzystuje się do przeglądania informacji z bazy danych; wówczas każdy rząd w kontrolce GtkCList odpowiada rzędowi w bazie danych, a każda kolumna – polu w bazie danych. Podczas tworzenia kontrolki GtkCList określamy stałą liczbę kolumn, której nie można już później zmienić. Jeśli zmiana liczby kolumn okaże się konieczna, trzeba usunąć i ponownie utworzyć kontrolkę. Kontrolkę GtkCList tworzymy przy pomocy funkcji gtk_clist_new, której przekazujemy żądaną liczbę kolumn, albo przy pomocy funkcji gtk_clist_new_with_ titles, której przekazujemy dodatkowo tablicę nagłówków kolumn. Aby
Drzewa, c-listy i zakładki
471
ustawić albo zmodyfikować nagłówki, możemy skorzystać z funkcji gtk_clist_set_column_title. /* --- Tworzymy tabelę o trzech kolumnach --- */ clista = gtk_clist_new (3); /* --- Nadajemy tytuły kolumnom --- */ gtk_clist_set_column_title (GTK_CLIST (clista), 0, "ID") gtk_clist_set_column_title (GTK_CLIST (clista), 1, "Nazwisko") gtk_clist_set_column_title (GTK_CLIST (clista), 2, "Adres")
Poniżej zamieszczamy krótszą wersję tego samego kodu: /* --- Definiujemy nagłówki --- */ char *szNaglowki[] = { "ID", "Nazwisko", "Adres"); /* --- Tworzymy c-listę z nagłówkami pojedynczą instrukcją --- */ clista = gtk_clist_new_with_titles (3, szNaglowki);
Dodawanie danych do GtkCList Można dołączyć dane do kontrolki GtkCList przy pomocy funkcji gtk_clist_append. Przyjmuje ona parametry w postaci c-listy oraz tablicy łańcuchów. Tablica powinna zawierać tyle elementów, ile kolumn ma kontrolka GtkCList. /* --- Dodajemy rząd statycznych danych --- */ char *szDane = {"0123", "Eryk", ul. Główna 123"}; /* --- Dodajemy rząd danych do c-listy --- */ gtk_clist_append (GTK_CLIST (clista), szDane);
Oprócz dołączania danych na końcu c-listy, możemy wstawić je w dowolnym punkcie przy pomocy funkcji gtk_clist_insert, przekazując jej indeks, w którym należy wstawić rząd. /* --- Wstawiamy rząd statycznych danych --- */ char *szDane = {"0123", "Eryk", ul. Główna 123"}; /* --- Wstawiamy dane za 10 rzędem --- */ gtk_clist_insert (GTK_CLIST (clista), 10, szDane);
Po dodaniu tekstu do c-listy możemy modyfikować go przy pomocy funkcji gtk_clist_set_text. Funkcja gtk_clist_get_text przyjmuje wskaźnik do łańcucha (char **) i wypełnia go danymi z określonego rzędu i kolumny.
472
Część IV Rozbudowa GTK+
Jeśli wskaźnik ma wartość NULL, oznacza to, że kontrolka GtkCList nie jest wypełniona. Funkcja zwraca niezerową wartość, jeśli uda jej się ustawić wskaźnik. Tekst nie jest kopią i nie należy go bezpośrednio modyfikować. char *dane; /* --- zmieniamy dane w rzędzie/kolumnie --- */ gtk_clist_set_text (GTK_CLIST (clista), 3, 1, "Jan"); /* --- Pobieramy tekst --- */ gtk_clist_get_text (GTK_CLIST (clista), 3, 1, &data);
Usuwanie rzędów Można usunąć rzędy c-listy przy pomocy funkcji gtk_clist_remove, przekazując jej indeks usuwanego rzędu. Jeśli jednak chcemy usunąć wszystkie rzędy, szybciej jest wywołać funkcję gtk_clist_clear. /* --- Usuwamy pierwszy rząd --- */ gtk_clist_remove (GTK_CLIST (clista), 0); /* --- Usuwamy wszystkie elementy c-listy --- */ gtk_clist_clear (GTK_CLIST (clista));
Przyspieszanie wstawiania i usuwania Podobnie jak kontrolka tekstu, c-lista może zawierać duże zbiory danych, zwłaszcza w przypadku wyświetlania tabel baz danych. Aby przyspieszyć proces wstawiania i usuwania informacji w GtkCList, kontrolkę można „zamrozić”, podobnie jak kontrolkę tekstową, aby zapobiec uaktualnianiu danych do czasu zakończenia modyfikacji danych. Funkcja gtk_clist_freeze zapobiega uaktualnianiu kontrolki aż do czasu wywołania funkcji gtk_clist_thaw. /* --- Zamrażamy kontrolkę --- */ gtk_clist_freeze (GTK_CLIST (clista)); /* --- przetwarzamy dane --- */ /* --- Odmrażamy listę --- */ gtk_clist_thaw (GTK_CLIST (clista));
Drzewa, c-listy i zakładki
473
Cechy nagłówków Pasek nagłówków posiada kilka właściwości, które aplikacja może zmodyfikować. Możemy zdecydować, czy w ogóle chcemy pokazywać nagłówki. Funkcja gtk_clist_column_titles_hide ukrywa pasek nagłówków; funkcja gtk_clist_column_titles_show go pokazuje. Zamiast tytułu możemy wyświetlić kontrolkę – funkcja gtk_clist_set_column_widget pozwala umieścić dowolną kontrolkę w pasku nagłówków, dzięki czemu możemy umieścić w nim na przykład rysunki. /* --- Ukrywamy pasek nagłówków --- */ gtk_clist_column_titles_hide (GTK_CLIST (clista)); /* --- Uwidaczniamy nagłówki --- */ gtk_clist_column_titles_show (GTK_CLIST (clista)); /* --- Umieszczamy w nagłówku uprzednio utworzoną piksmapę --- */ gtk_clist_set_column_widget (GTK_CLIST (clista), 1, piksmapa);
Parametry rzędów i kolumn Podczas tworzenia kontrolki GtkCList trzeba samodzielnie dostosować szerokość kolumn. Kontrolka nie wie, jak szerokie powinny być kolumny, i opiera domyślną szerokość kolumny na szerokości nagłówka. Aby ustawić szerokość dowolnej kolumny w c-liście, możemy skorzystać z funkcji gtk_clist_set_column_width. Możemy także ustalić wysokość rzędu przy pomocy funkcji gtk_clist_set_row_height, ale jeśli nie zmieniamy czcionki kontrolki albo nie dodajemy obszernych grafik, ustawianie wysokości rzędu nie powinno być potrzebne. /* --- Ustawiamy szerokość kolumny na 100 pikseli --- */ gtk_clist_set_column_width (GTK_CLIST (clista), 0, 100); /* --- Zmieniamy wysokość rzędu --- */ gtk_clist_set_row_height (GTK_CLIST (clista), 25);
Dane w kolumnach można również wyświetlać z wyrównaniem do środka albo do prawej lub lewej strony. Funkcja gtk_clist_set_column_ justification przyjmuje kontrolkę, kolumnę i parametr typu GtkJustification. Dozwolonymi wartościami dla GtkJustification są GTK_JUSTIFY_LEFT, GTK_JUSTIFY_RIGHT, GTK_JUSTIFY_CENTER i GTK_JUSTIFY_FILL.
474
Część IV Rozbudowa GTK+
Grafika w kontrolce GtkCList Do tej pory omawialiśmy tylko wyświetlanie informacji tekstowych. Patrzenie na długie kolumny tekstu i liczb może być zniechęcające, więc clista pozwala na umieszczanie w rzędach także piksmap. Piksmapę wstawiamy przy pomocy funkcji gtk_clist_set_pixmap; możemy także ją sprawdzić przy pomocy funkcji gtk_clist_get_pixmap. Ważna uwaga: kiedy dodajemy do listy informacje tekstowe (przy pomocy funkcji wstawiających albo dołączających), kolumny, które wyświetlają piksmapy, powinny mieć tekst ustawiony na NULL. W przeciwnym przypadku piksmapy nie zostaną wyświetlone. /* --- Dodajemy piksmapę --- */ gtk_clist_set_pixmap (GTK_CLIST (clista), rzad, kol, piks, maska); /* --- Sprawdzamy istniejącą piksmapę --- */ gtk_clist_get_pixmap (GTK_CLIST (clista), rzad, kol, &piks, &maska);
Przykładowa aplikacja Poniższa aplikacja wykorzystuje kontrolki GtkNotebook i GtkCList, aby wyświetlić statystyki z dziennika serwera WWW Apache. Poszczególne statystyki znajdują się na trzech stronach notatnika. Pierwsza wyświetla ruch w sieci według godzin (patrz rysunek 14.3). Informacje te pozwalają stwierdzić, czy w godzinach szczytu dysponujemy odpowiednią przepustowością. Druga strona wyświetla ruch według dni, zaczynając od początku pliku dziennika, co pozwala zauważyć pewne trendy – być może reklama zamieszczona w jednym z serwisów spowodowała gwałtowny wzrost ruchu, czego zapewne wcześniej nie zauważyliśmy. Trzecia strona pokazuje ruch skierowany do stron użytkowników. Aplikacja traktuje stronę domową serwisu jako odrębnego użytkownika (*ROOT). Każda lista GtkCList będzie zawierać piksmapy, które będą graficznie obrazować ruch, czy to dzienny, czy godzinowy, czy według użytkownika. Dane te pozwolą administratorowi szybko zauważyć problemy – na przykład serwis dla dorosłych, prowadzony na domowej stronie któregoś z użytkowników. Dużo szybciej jest rzucić okiem na wykres, niż przeglądać rzędy liczb w poszukiwaniu informacji.
Drzewa, c-listy i zakładki
475
Rysunek 14.3. Ruch godzinowy.
Wiele plików wchodzących w skład projektu napisaliśmy wcześniej – ponowne użycie tego samego kodu jest jedną z idei przewodnich książki. Pliki rozne.c, postep.c i wybpliku.c omówiliśmy w poprzednich rozdziałach. Plik interfejs.c został nieco zmodyfikowany, aby uwzględnić zmieniony interfejs aplikacji. Najważniejsze pliki projektu to moduł analizujący dziennik (analiza.c), moduł wyświetlający notatnik i c-listy (notatnik.c) oraz generator piksmap (bitmapy.c). Piksmapy wyświetlane w tabeli mają szczególną właściwość: są generowane w locie. Zamiast używać statycznych danych dla piksmap, używamy generującej je procedury, która przydziela miejsce na piksmapę i tworzy ją na podstawie rozmiaru poziomego paska, który chcemy wyświetlić. Generowanie poziomego, graficznego paska jest nieskomplikowane, a zarazem elastyczniejsze, niż tworzenie statycznych danych dla piksmapy. Możemy łatwo zmienić rozmiar i rozdzielczość wykresu, modyfikując procedurę, która tworzy dane piksmapy. Gdybyśmy jednak zdecydowali się użycie statycznych rysunków, spędzilibyśmy mnóstwo czasu nad edycją wszystkich możliwych piksmap, z których tworzone są paski wykresu.
typydziennika.h Plik typydziennika.h definiuje struktury danych, używane w aplikacji. Każda struktura przechowuje konkretny zbiór danych, które musimy uwzględnić w aplikacji.
Część IV Rozbudowa GTK+
476
Struktura typZnacznikCzasowy przechowuje datę i czas, w którym nastąpiło „trafienie” w stronę WWW. typedef struct { int rok; int miesiac; int dzien; int godziny; int minuty; int sekundy; } typZnacznikCzasowy;
Struktura typTrafienie przechowuje wszystkie informacje o trafieniu w stronę WWW. typedef struct { char *sIp; char *sUzytk; char *sData; char *sPol; char *sURL; int nRezultat; int nRozmiar; typZnacznikCzasowy data; } typTrafienie;
Struktura typStat przechowuje liczbę trafień w URL i rozmiar przesłanych danych (w bajtach). typedef struct { char *sURL; long nTrafienia; long nRozmiar; } typStat;
Struktura typData przechowuje datę i służy do analizowania ruchu według daty. typedef struct { int rok;
Drzewa, c-listy i zakładki
477
int miesiac; int dzien; } typData;
Struktura typDaneDaty przechowuje liczbę trafień i rozmiar przesłanych danych (w bajtach) w jednym dniu.. typedef struct { long nTrafienia; long nRozmiar; typData *data; } typDaneDaty;
analiza.c Aby można było wyświetlić informacje, trzeba je odczytać i podsumować. Dane są przechowywane w drzewie GTree, dzięki czemu sprawdzanie i pobieranie informacji przebiega szybko. Dziennik serwera zawiera wiele rzędów danych, a my potrzebujemy szybkiego dostępu do aktualizowanej informacji. Każdy rekord z dziennika jest wczytywany, analizowany i zachowywany jako trafienie. Trafienie jest następnie przekazywane do różnych funkcji, które tworzą podsumowanie na podstawie danych zawartych w trafieniu. Jeśli na przykład pobrano stronę konkretnego użytkownika w konkretnym dniu, trafienie zostanie dodane do sumarycznych danych użytkownika oraz do sumarycznych danych dnia. Kiedy analiza pliku dziennika dobiegnie końca, wówczas można wyświetlić go w c-liście na podstawie sumarycznych danych. /* * Plik: analog.c * Auth: Eric Harlow * * Analizuje plik dziennika serwera Apache * i podsumowuje zawarte w nim informacje. */ #include #include #include #include #include
478
Część IV Rozbudowa GTK+
#include #include #include "typydziennika.h" void Inicjacja (); void SledzUzytkownikow (typTrafienie *trafienie); void SledzDaty (typTrafienie *trafienie); void SledzPliki (typTrafienie *trafienie); void ZacznijPostep (); void UaktualnijPostep (long poz, long dlug); void ZakonczPostep (); void UaktualnijStatystyki (typTrafienie *trafienie); /* * Tablice do przechowywania danych godzinowych */ long trafieniaOGodzinie[24]; long rozmiarOGodzinie[24]; /* * Drzewa przechowujące różne typy danych */ GTree *drzewoPlikow = NULL; GTree *drzewoDat = NULL; GTree *drzewoUzytk = NULL; void AnalizujLinie (char *bufor); #define MIESIACE 12 char *sPoprawneMiesiace[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; /* * DataNaLancuch * * Zamienia datę na łańcuch * data - data do przekształcenia * bufor - char* wystarczająco duży, aby przechować datę/czas. */ void DataNaLancuch (typZnacznikCzasowy *data, char *bufor) { sprintf (bufor, "%d/%d/%d %d:%d:%d ", data->miesiac,
Drzewa, c-listy i zakładki
data->dzien, data->rok, data->godziny, data->minuty, data->sekundy); } /* * PrzeksztalcDate * * Przekształca datę z formatu 30/Feb/1999:11:42:23, * w którym serwer Apache zapisuje daty w swoich dziennikach. */ void PrzeksztalcDate (char *sData, typZnacznikCzasowy *data) { char sMiesiac[33]; int i; /* --- Break down the data into its components --- */ sscanf (sData, "%d/%3c/%d:%d:%d:%d", &data->dzien, sMiesiac, &data->rok, &data->godziny, &data->minuty, &data->sekundy); /* --- Zamieniamy datę tekstową na liczbową --- */ for (i = 0; i < MIESIACE; i++) { /* --- Czy to miesiąc? --- */ if (!strncasecmp (sPoprawneMiesiace[i], sMiesiac, strlen
➥(sPoprawneMiesiace[i]))) { data->miesiac = i + 1; break; } } } /* * AnalizujDziennik * * Analizuje plik dziennika i organizuje zawarte w nim dane
479
Część IV Rozbudowa GTK+
480
* w formacie, który będziemy mogli poddać interpretacji. * * sPlik - Plik do odczytania */ void AnalizujDziennik (char *sPlik) { FILE *fp; char bufor[350]; long nDlugoscPliku = 0; struct stat statusPliku; /* --- Przydzielamy drzewa i inicjujemy dane --- */ Inicjacja (); /* --- Pobieramy nazwę pliku --- */ stat (sPlik, &statusPliku); nDlugoscPliku = statusPliku.st_size; /* --- Otwieramy le plik data.godziny]++; rozmiarOGodzinie[trafienie->data.godziny] += trafienie->nRozmiar; /* --- Uaktualniamy pozostałe informacje --- */ SledzPliki (trafienie); SledzDaty (trafienie); SledzUzytkownikow (trafienie); } /* * SledzUzytkownikow * * Śledzi ruch według stron użytkowników */ void SledzUzytkownikow (typTrafienie *trafienie)
Drzewa, c-listy i zakładki
{ typStat *stat; char *sUzytkownik; /* --- Złe dane --- */ if (trafienie->sURL == NULL || strlen (trafienie->sURL) == 0) return; /* --- Pobieramy pierwszą część ścieżki --- */ sUzytkownik = strtok (&trafienie->sURL[1], "/"); if (sUzytkownik == NULL) return; /* --- Jeśli jest to strona domowa użytkownika --- */ if (sUzytkownik[0] == '~') { /* --- Pobieramy nazwę --- */ sUzytkownik = &sUzytkownik[1]; /* --- Jeśli ~ było zakodowane jako %7E --- */ } else if (!strcmp (sUzytkownik, "%7E")) { /* --- Pobieramy nazwę --- */ sUzytkownik = &sUzytkownik[3]; } else { /* --- Nie jest to strona użytkownika --- */ sUzytkownik = "*ROOT"; } /* --- Sprawdzamy, czy użytkownik już istnieje w drzewie --- */ stat = g_tree_lookup (drzewoUzytk, sUzytkownik); /* --- Jeśli użytkownik nie istnieje --- */ if (stat == NULL) { /* --- Tworzymy miejsce na użytkownika --- */ stat = g_malloc (sizeof (typStat)); /* --- Wypełniamy pola --- */ stat->sURL = strdup (sUzytkownik); stat->nTrafienia = 0; stat->nRozmiar = 0; /* --- Wstawiamy użytkownika do drzewa --- */
483
Część IV Rozbudowa GTK+
484 g_tree_insert (drzewoUzytk, stat->sURL, stat); }
/* --- Uaktualniamy liczbę trafień dla użytkownika --- */ stat->nTrafienia ++; stat->nRozmiar += trafienie->nRozmiar; } /* * SledzPliki * * Śledzi liczbę trafień w poszczególne pliki */ void SledzPliki (typTrafienie *trafienie) { typStat *stat; /* --- Wyszukujemy URL w drzewie --- */ stat = g_tree_lookup (drzewoPlikow, trafienie->sURL); /* --- Jeśli nie znajdziemy URL-a --- */ if (stat == NULL) { /* --- Tworzymy węzeł dla URL-a --- */ stat = g_malloc (sizeof (typStat)); /* --- Wypełniamy węzeł --- */ stat->sURL = strdup (trafienie->sURL); stat->nTrafienia = 0; stat->nRozmiar = 0; /* --- Dodajemy węzeł do drzewa --- */ g_tree_insert (drzewoPlikow, stat->sURL, stat); } /* --- Uaktualniamy liczbę trafień w plik --- */ stat->nTrafienia ++; stat->nRozmiar = trafienie->nRozmiar; } /* * SledzDaty * * Śledzi ruch w poszczególnych dniach
Drzewa, c-listy i zakładki
*/ void SledzDaty (typTrafienie *trafienie) { typData data; typData *nowadata; typDaneDaty *info; /* --- Pobieramy datę trafienia --- */ data.rok = trafienie->data.rok; data.miesiac = trafienie->data.miesiac; data.dzien = trafienie->data.dzien; /* --- Wyszukujemy dane --- */ info = g_tree_lookup (drzewoDat, &data); /* --- Nie ma danych dla daty? --- */ if (info == NULL) { /* --- Tworzymy pole przechowujące dane daty --- */ info = g_malloc (sizeof (typDaneDaty)); nowadata = g_malloc (sizeof (typData)); /* --- Wypełniamy pola --- */ *nowadata = data; info->nTrafienia = 0; info->nRozmiar = 0; info->data = nowadata; /* --- Dodajemy dane daty do drzewa --- */ g_tree_insert (drzewoDat, nowadata, info); } /* --- Uaktualniamy liczbę trafień dla daty --- */ info->nTrafienia ++; info->nRozmiar += trafienie->nRozmiar; } /* * PorownajLancuchy * * Funkcja do porównywania dwóch łańcuchów, wykorzystywana * jako funkcja zwrotna dla drzewa. Moglibyśmy umieścić funkcję * strcmp bezpośrednio jako funkcję zwrotną drzewa, ale
485
Część IV Rozbudowa GTK+
486
* w przyszłości możemy zechcieć zmienić sposób porównywania. */ gint PorownajLancuchy (gpointer g1, gpointer g2) { return (strcmp ((char *) g1, (char *) g2)); } /* * PorownajDaty * * Porównuje dwie daty i zwraca wartość ujemną, jeśli pierwsza * data jest mniejsza od drugiej, dodatnią, jeśli pierwsza * data jest większa od drugiej, i zero, jeśli daty są równe. */ gint PorownajDaty (gpointer g1, gpointer g2) { typData *d1 = (typData *) g1; typData *d2 = (typData *) g2; /* --- Rok ma najwyższe pierwszeństwo --- */ if (d1->rok == d2->rok) { /* --- Lata te same, a miesiące? --- */ if (d1->miesiac == d2->miesiac) { /* --- Zwracamy różnicę pomiędzy dniami --- */ return (d1->dzien - d2->dzien); } else { /* --- Miesiące są różne - obliczamy przyrost --- */ return (d1->miesiac - d2->miesiac); } } else { /* --- Lata są różne - obliczamy przyrost --- */ return (d1->rok == d2->rok); } } /* * PobierzTrafieniaOGodzinie * * Funkcja zwraca liczbę trafień dla danego czasu dnia
Drzewa, c-listy i zakładki
487
*/ void PobierzTrafieniaOGodzinie (int nGodziny, long *trafienia, long *rozmiar) { /* --- Jeśli zegar jest poza zakresem --- */ if (nGodziny < 0 || nGodziny > 23) { *trafienia = 0; *rozmiar = 0; } else { *trafienia = trafieniaOGodzinie[nGodziny]; *rozmiar = rozmiarOGodzinie[nGodziny]; } } /* * Inicjacja * * Inicjuje dane, aby można było wczytać plik dziennika. * Tworzy drzewa, przechowujące informacje z dziennika. */ void Inicjacja () { int i; /* --- Tworzymy drzewa do przechowywania informacji --- */ drzewoPlikow = g_tree_new (PorownajLancuchy); drzewoDat = g_tree_new (PorownajDaty); drzewoUzytk = g_tree_new (PorownajLancuchy); /* --- Oczyszczamy tablicę z trafieniami według godzin --- */ for (i = 0; i < 24; i++) { trafieniaOGodzinie[i] = 0; rozmiarOGodzinie[i] = 0; } } /* * ZwolnijURLe * * Zwalniamy pamięć przydzieloną na URL-e. * Jest to funkcja zwrotna obchodu drzewa. */
Część IV Rozbudowa GTK+
488
gint ZwolnijURLe (gpointer klucz, gpointer wartosc, gpointer dane) { typStat *info; info = (typStat *) wartosc; free (info->sURL); g_free (info); return (0); } /* * ZwolnijDaty * * Zwalniamy pamięć przydzieloną na śledzenie ruchu * według dat. Jest to funkcja zwrotna obchodu drzewa. */ gint ZwolnijDaty (gpointer klucz, gpointer wartosc, gpointer dane) { typDaneDaty *info; info = (typDaneDaty *) wartosc; g_free (info->data); g_free (info); return (0); } /* * ZwolnijZasoby * * Zwalnia pamięć używaną przez węzły drzewa, a następnie * usuwa drzewa. */ void ZwolnijZasoby () { /* --- Zwalniamy dane przechowywane w drzewie --- */ g_tree_traverse (drzewoUzytk, ZwolnijURLe, G_IN_ORDER, NULL); g_tree_traverse (drzewoDat, ZwolnijDaty, G_IN_ORDER, NULL); g_tree_traverse (drzewoPlikow, ZwolnijURLe, G_IN_ORDER, NULL); /* --- Usuwamy drzewa --- */ g_tree_destroy (drzewoUzytk); g_tree_destroy (drzewoDat);
Drzewa, c-listy i zakładki
489
g_tree_destroy (drzewoPlikow); /* --- Zerujemy wskaźniki --- */ drzewoUzytk = NULL; drzewoDat = NULL; drzewoPlikow = NULL; }
bitmapy.c Kontrolka GtkClist wyświetla wykres, który ilustruje ruch w serwisie, według dnia albo godziny. Zamiast tworzyć kilka plików .xpm z piksmapami, lepiej użyć kodu, który dynamicznie wygeneruje dane xpm. Dzięki temu łatwo jest zmienić szerokość wykresu. Zamiast przerysowywać poszczególne paski, możemy po prostu określić, jak szeroki powinien być wykres. Zauważmy, że wykres jest poziomy, dzięki czemu dane piksmapy są takie same dla każdego rzędu. Możemy wykorzystać to spostrzeżenie i użyć tego samego łańcucha we wszystkich rzędach danych piksmapy. #include #include /* * UtworzBitmapePaska * * Tworzy piksmapę o zadanych charakterystykach * * wysok - wysokość tworzonej piksmapy * szerok - szerokość tworzonej piksmapy * rozmiar - jak długi powinien być pasek * sKolor - kolor wypełnienia paska */ char **UtworzBitmapePaska (int wysok, int szerok, int rozmiar, char *sKolor) { char **sBitmapa; char *nowybufor; char bufor[88]; int i; /* --- Przydzielamy miejsce na dane --- */ sBitmapa = g_malloc ((wysok + 1 + 2) * sizeof (gchar *));
Część IV Rozbudowa GTK+
490
/* --- Tworzymy nagłówek piksmapy - wysokość/szerokość/ kolory/liczba znaków na kolor --- */ sprintf (bufor, "%d %d 2 1", szerok, wysok); sBitmapa[0] = g_strdup (bufor); /* --- Definiujemy kolor przezroczysty ("none") --- */ sBitmapa[1] = g_strdup (" c None"); /* --- Definiujemy kolor wypełnienia --- */ sprintf (bufor, "X c %s", sKolor); sBitmapa[2] = g_strdup (bufor); /* --- Wypełniamy bufor na podstawie rozmiaru --- */ strcpy (bufor, " "); for (i = 0; i < rozmiar; i++) { strcat (bufor, "X"); } /* --- Reszta zostaje niewypełniona --- */ while (i < szerok) { strcat (bufor, " "); i++; } /* --- Dodajemy spację --- */ strcat (bufor, " "); /* --- Kopiujemy łańcuch --- */ nowybufor = g_strdup (bufor); /* --- Takie same dane dla wszystkich rzędów piksmapy --- */ for (i = 3; i < wysok+3; i++) { sBitmapa[i] = nowybufor; } /* --- Zwracamy utworzoną piksmapę --- */ return (sBitmapa); } /* * ZwolnijBitmapePaska * * Zwalniamy pamięć, przydzieloną na dane bitmapy.
Drzewa, c-listy i zakładki
491
*/ void ZwolnijBitmapePaska (char **bitmapa) { g_free (bitmapa[0]); g_free (bitmapa[1]); g_free (bitmapa[2]); g_free (bitmapa[3]); g_free (bitmapa); }
notatnik.c Po załadowaniu danych do drzew wywołujemy zamieszczone niżej procedury, które wyświetlają dane w poszczególnych kontrolkach GtkCList. Kontrolki tworzone są na oddzielnych stronach, a użytkownik może zmieniać strony, aby obejrzeć zawarte na nich informacje. Dane dla kontrolek GtkClist tworzone są poprzez obchód drzewa. W trakcie obchodu pobieramy i wyświetlamy informacje tekstowe (data, użytkownik, trafienie itd.) oraz obliczmy maksymalny ruch. Po ustaleniu maksymalnego ruchu obchodzimy drzewo ponownie, aby wyświetlić wykresy. Obliczenie maksymalnego natężenia ruchu jest niezbędne, ponieważ wykresy są rysowane względem największej wartości i nie mogą zostać wyświetlone, dopóki jej nie ustalimy.
Rysunek 14.4. Ruch dzienny.
492
Część IV Rozbudowa GTK+
/* * Autor: Eric Harlow * Plik: notatnik.c * * Aplikacja korzystająca z kontrolki notatnika. */ #include #include "typydziennika.h" extern GTree *drzewoDat; extern GTree *drzewoUzytk; GtkWidget *stronaGodziny = NULL; GtkWidget *stronaDni = NULL; GtkWidget *stronaUzytk = NULL; GtkWidget *clistaGodziny = NULL; GtkWidget *clistaDni = NULL; GtkWidget *clistaUzytk = NULL; typedef struct { GtkWidget *kontrolka; long nMaksRozmiar; long rzad; } typDaneWykresu; /* * Tytuły wyświetlane w C-listach na różnych stronach * Titles displayed on the clist for the various pages */ char *szTytulyGodziny[] = {"Godzina", "Trafienia", "Rozmiar", "Wykres"}; char *szTytulyDni[] = {"Data", "Trafienia", "Rozmiar", "Wykres"}; char *szTytulyUzytk[] = {"Użytkownik", "Trafienia", "Rozmiar", "Wykres"}; #define LICZBA_WYKRESOW 21 GdkPixmap *piksmapyWykresow [LICZBA_WYKRESOW]; GdkBitmap *maski[LICZBA_WYKRESOW]; char **UtworzBitmapePaska (int wysok, int szerok, int rozmiar, char *sKolor); void ZwolnijZasoby (); void WypelnijUzytk (); void WypelnijDni (); void WypelnijGodziny ();
Drzewa, c-listy i zakładki
493
void PobierzTrafieniaWGodzinie (int nGodziny, long *trafienia, long *rozmiar); void ZwolnijBitmapePaska (char **bitmapa); /* * GenerujPiksmapy * * Generuje piksmapy dla poziomych pasków wykresu o wszystkich * rozmiarach, które obsługuje aplikacja. */ void GenerujPiksmapy (GtkWidget *kontrolka) { int i; gchar **piksmapa_d; /* --- Dla każdego możliwego wykresu --- */ for (i = 0; i < LICZBA_WYKRESOW; i++) { /* --- Pobieramy dane dla wykresu --- */ piksmapa_d = UtworzBitmapePaska (9, 65, i * 3, "#ff0000"); /* --- Tworzymy piksmapę --- */ piksmapyWykresow[i] = gdk_pixmap_create_from_xpm_d ( kontrolka->window, &maski[i], NULL, (gpointer) piksmapa_d); /* --- Zwalniamy dane --- */ ZwolnijBitmapePaska (piksmapa_d); } } /* * ZmianaStrony * * Zdarzenie to występuje, kiedy ognisko przesuwa * się na inną stronę. */ static void ZmianaStrony (GtkWidget *kontrolka, GtkNotebookPage *strona, gint numer_strony) { }
Część IV Rozbudowa GTK+
494
/* * DodajStrone * * Dodaje stronę do notatnika * * notatnik - istniejący notatnik * szNazwa - nazwa dodawanej strony */ GtkWidget *DodajStrone (GtkWidget *notatnik, char *szNazwa) { GtkWidget *etykieta; GtkWidget *ramka; /* --- Tworzymy etykietę na podstawie nazwy --- */ etykieta = gtk_label_new (szNazwa); gtk_widget_show (etykieta); /* --- Tworzymy ramkę na stronie --- */ ramka = gtk_frame_new (szNazwa); gtk_widget_show (ramka); /* --- Dodajemy stronę z ramką i etykietą --- */ gtk_notebook_append_page (GTK_NOTEBOOK etykieta);
(notatnik),
return (ramka); } /* * UtworzNotatnik * * Tworzy nowy notatnik i dodaje do niego strony. * * okno - okno, w którym znajdzie się notatnik. */ void UtworzNotatnik (GtkWidget *okno) { GtkWidget *notatnik; /* --- tworzymy notatnik --- */ notatnik = gtk_notebook_new (); /* --- Sprawdzamy wystąpienie zdarzenia switch_page --- */
ramka,
Drzewa, c-listy i zakładki
495
gtk_signal_connect (GTK_OBJECT (notatnik), "switch_page", GTK_SIGNAL_FUNC (ZmianaStrony), NULL); /* --- Zakładki mają znajdować się na górze --- */ gtk_notebook_set_tab_pos (GTK_NOTEBOOK GTK_POS_TOP);
(notatnik),
/* --- Dodajemy notatnik to okna --- */ gtk_box_pack_start (GTK_BOX (okno), notatnik, TRUE, TRUE, 0); /* --- Ustawiamy obramowanie notatnika --- */ gtk_container_border_width (GTK_CONTAINER (notatnik), 10); /* --- Dodajemy strony do notatnika --- */ stronaGodziny = DodajStrone (notatnik, "Ruch godzinowy"); stronaDni = DodajStrone (notatnik, "Ruch dzienny"); stronaUzytk = DodajStrone (notatnik, "Ruch wg. użytkownika"); /* --- Pokazujemy wszystkie kontrolki. --- */ gtk_widget_show_all (okno); } /* * WypelnijStrony * * Wypełnia strony notatnika informacjami z dziennika. * Wypełnia stronę godzinową, dzienną i wg. użytkownika. * * Zwania dane, wykorzystane do wygenerowania stron. */ void WypelnijStrony () { /* --- Zwalniamy dane C-list, jeśli listy były już używane --- */ if (clistaUzytk) { gtk_clist_clear (GTK_CLIST (clistaUzytk)); } if (clistaGodziny) { gtk_clist_clear (GTK_CLIST (clistaGodziny)); } if (clistaDni) { gtk_clist_clear (GTK_CLIST (clistaDni)); }
Część IV Rozbudowa GTK+
496 /* --- Wypełniamy wszystkie strony --- */ WypelnijGodziny (); WypelnijDni (); WypelnijUzytk (); /* --- Zwalniamy zasoby, przydzielone podczas analizy dziennika --- */ ZwolnijZasoby (); }
/* * WypelnijGodziny * * Wypełnia C-listę danymi o ruchu godzinowym. * Zakładamy, że drzewa są wypełnione gotowymi do * użycia danymi. */ void WypelnijGodziny () { gchar *strWartosc[4]; int i; int ix; long trafienia; long rozmiar; gchar bufor0[88]; gchar bufor1[88]; gchar bufor2[88]; long nMaksRozmiar = 0; /* --- Tablica, używana do wstawiania danych do C-listy --- */ strWartosc[0] = bufor0; strWartosc[1] = bufor1; strWartosc[2] = bufor2; /* --- Tutaj mamy NULL, ponieważ jest to piksmapa --- */ strWartosc[3] = NULL; /* --- Jeśli c-lista nie jest jeszcze utworzona... --- */ if (clistaGodziny == NULL) { /* --- Tworzymy c-listę o czterech kolumnach --- */ clistaGodziny = gtk_clist_new_with_titles (4, szTytulyGodziny);
Drzewa, c-listy i zakładki
497
/* --- Uwidaczniamy nagłówki kolumn --- */ gtk_clist_column_titles_show (GTK_CLIST (clistaGodziny)); /* --- Ustawiamy szerokości kolumn --- */ gtk_clist_set_column_width (GTK_CLIST (clistaGodziny), 0, 80); gtk_clist_set_column_width (GTK_CLIST (clistaGodziny), 1, 80); gtk_clist_set_column_width (GTK_CLIST (clistaGodziny), 2, 80); gtk_clist_set_column_width (GTK_CLIST (clistaGodziny), 3, 40); /* --- Ustawiamy justowanie każdej z kolumn --- */ gtk_clist_set_column_justification (GTK_CLIST (clistaGodziny), 0, GTK_JUSTIFY_RIGHT); gtk_clist_set_column_justification (GTK_CLIST (clistaGodziny), 1, GTK_JUSTIFY_RIGHT); gtk_clist_set_column_justification (GTK_CLIST (clistaGodziny), 2, GTK_JUSTIFY_RIGHT); /* --- Dodajemy c-listę do właściwej strony --- */ gtk_container_add (GTK_CONTAINER (stronaGodziny), clistaGodziny); } /* --- Generujemy rząd dla każdej z 24 godzin dnia --- */ for (i = 0; i < 24; i++) { /* --- Pokazujemy czas - np. 3:00 --- */ sprintf (strWartosc[0], "%d:00", i); /* --- Pobieramy liczbę trafień w tej godzinie --- */ PobierzTrafieniaOGodzinie (i, &trafienia, &rozmiar); /* --- Wyświetlamy liczbę trafień i przesłanych bajtów --- */ sprintf (strWartosc[1], "%ld", trafienia); sprintf (strWartosc[2], "%ld", rozmiar); /* --- Dodajemy dane do c-listy --- */ gtk_clist_append (GTK_CLIST (clistaGodziny), strWartosc); /* --- Zapamiętujemy największy blok przesłanych danych --- */ if (rozmiar > nMaksRozmiar) { nMaksRozmiar = rozmiar; } } /*
Część IV Rozbudowa GTK+
498
* Po wygenerowaniu c-listy musimy dodać do niej poziome * wykresy. Nie mogliśmy zrobić tego wcześniej, ponieważ nie * wiedzieliśmy, jaka jest maksymalna wartość. */ /* --- Dla każdej godziny dnia --- */ for (i = 0; i < 24; i++) { /* --- Pobieramy trafienia w tej godzinie --- */ PobierzTrafieniaOGodzinie (i, &trafienia, &rozmiar); /* --- Obliczamy długość wykresu --- */ ix = (rozmiar * LICZBA_WYKRESOW-1) / nMaksRozmiar; /* --- Wyświetlamy wykres w c-liście --- */ gtk_clist_set_pixmap (GTK_CLIST (clistaGodziny), i, 3, (GdkPixmap *) piksmapyWykresow[ix], maski[ix]); } /* --- Pokazujemy c-listę --- */ gtk_widget_show_all (GTK_WIDGET (clistaGodziny)); } /* * PokazDaneDnia * * Pokazuje informacje o ruchu w danym dniu. * Umieszcza je na c-liście, która wyświetla wykres * ruchu dziennego. * * Wywoływana przez funkcję zwrotną obchodu drzewa! */ gint PokazDaneDnia (gpointer klucz, gpointer wartosc, gpointer dane) { char *strWartosc[4]; typDaneDaty *daneDnia; long *pnMaks; char bufor0[88]; char bufor1[88]; char bufor2[88]; /* --- Pobieramy przekazane dane --- */ daneDnia = (typDaneDaty *) wartosc;
Drzewa, c-listy i zakładki
pnMaks = (long *) dane; /* --- Ustawiamy struktury, wypełniające c-listę --- */ strWartosc[0] = bufor0; strWartosc[1] = bufor1; strWartosc[2] = bufor2; strWartosc[3] = NULL; /* --- Umieszczamy datę w pierwszej kolumnie --- */ sprintf (strWartosc[0], "%02d/%02d/%4d", daneDnia->data->miesiac, daneDnia->data->dzien, daneDnia->data->rok); /* --- Wpisujemy trafienia i ilość przesłanych danych --- */ sprintf (strWartosc[1], "%ld", daneDnia->nTrafienia); sprintf (strWartosc[2], "%ld", daneDnia->nRozmiar); /* --- Dodajemy dane do c-listy --- */ gtk_clist_append (GTK_CLIST (clistaDni), strWartosc); /* --- Zapamiętujemy maksymalną wartość --- */ if (*pnMaks < daneDnia->nRozmiar) { *pnMaks = daneDnia->nRozmiar; } /* --- 0 => kontynuujemy obchód --- */ return (0); } /* * PokazDaneUzytk * * Pokazuje informacje o użytkowniku (bez wykresów), ale * zachowuje maksymalną liczbę bajtów, aby można było * później wygenerować wykres. * * Wywoływana przez funkcję zwrotną obchodu drzewa! */ gint PokazDaneUzytk (gpointer klucz, gpointer wartosc, gpointer dane) { char *strWartosc[4]; typStat *info;
499
Część IV Rozbudowa GTK+
500 long *pnMaks; char bufor0[88]; char bufor1[88]; char bufor2[88]; /* --- Pobieramy przekazane dane --- */ info = (typStat *) wartosc; pnMaks = (long *) dane; /* --- Bufory do dołączania danych --- */ strWartosc[0] = bufor0; strWartosc[1] = bufor1; strWartosc[2] = bufor2; strWartosc[3] = NULL;
/* --- Aktualizujemy URL w pierwszej kolumnie --- */ sprintf (strWartosc[0], "%s", info->sURL); /* --- Aktualizujemy trafienia i rozmiar w bajtach --- */ sprintf (strWartosc[1], "%ld", info->nTrafienia); sprintf (strWartosc[2], "%ld", info->nRozmiar); /* --- Dodajemy dane do c-listy --- */ gtk_clist_append (GTK_CLIST (clistaUzytk), strWartosc); /* --- Zapamiętujemy maksymalny rozmiar --- */ if (info->nRozmiar > *pnMaks) { *pnMaks = info->nRozmiar; } return (0); } /* * PokazWykres * * Wyświetla dzienny wykres. * * Wywoływana jako funkcja zwrotna obchodu drzewa */ gint PokazWykres (gpointer klucz, gpointer wartosc, gpointer dane) { int ix;
Drzewa, c-listy i zakładki
typDaneWykresu *daneWykresu = (typDaneWykresu *) dane; typDaneDaty *daneDnia = (typDaneDaty *) wartosc; /* --- Określamy, który wykres należy wyświetlić --- */ ix = (daneDnia->nRozmiar * LICZBA_WYKRESOW-1) /
➥daneWykresu->nMaksRozmiar; /* --- Ustawiamy piksmapę w c-liście --- */ gtk_clist_set_pixmap (GTK_CLIST (daneWykresu->kontrolka), daneWykresu->rzad, 3, piksmapyWykresow[ix], maski[ix]); /* --- Następny rząd do wyświetlenia --- */ daneWykresu->rzad++; /* --- Kontynuujemy... --- */ return (0); } /* * WypelnijDni * * Wypełnia c-listę danymi z drzewa, Zakłada, że * drzewo jest całkowicie wypełnione danymi. */ void WypelnijDni () { gchar *strWartosc[4]; long nMaksDzien; gchar bufor0[88]; gchar bufor1[88]; gchar bufor2[88]; typDaneWykresu daneWykresu; /* --- tworzymy tabelę --- */ strWartosc[0] = bufor0; strWartosc[1] = bufor1; strWartosc[2] = bufor2; /* --- NULL - tu będzie umieszczona grafika. --- */ strWartosc[3] = NULL; /* --- Jeśli c-lista jeszcze nie została utworzona... --- */
501
Część IV Rozbudowa GTK+
502 if (clistaDni == NULL) {
/* --- Tworzymy c-listę --- */ clistaDni = gtk_clist_new_with_titles (4, szTytulyDni); /* --- Wyświetlamy nagłówki kolumn --- */ gtk_clist_column_titles_show (GTK_CLIST (clistaDni)); /* --- Ustawiamy szerokości kolumn --- */ gtk_clist_set_column_width (GTK_CLIST (clistaDni), 0, 80); gtk_clist_set_column_width (GTK_CLIST (clistaDni), 1, 80); gtk_clist_set_column_width (GTK_CLIST (clistaDni), 2, 80); /* --- Ustawiamy justowanie kolumn --- */ gtk_clist_set_column_justification (GTK_CLIST (clistaDni), 0, GTK_JUSTIFY_RIGHT); gtk_clist_set_column_justification (GTK_CLIST (clistaDni), 1, GTK_JUSTIFY_RIGHT); gtk_clist_set_column_justification (GTK_CLIST (clistaDni), 2, GTK_JUSTIFY_RIGHT); /* --- Dodajemy c-listę do strony notatnika --- */ gtk_container_add (GTK_CONTAINER (stronaDni), clistaDni); } /* --- Ustawiamy maksymalny rozmiar na zero --- */ nMaksDzien = 0; /* * --- Obchodzimy drzewo i wyświetlamy informacje tekstowe, * zapamiętując maksymalną wartość, aby można było * wyświetlić wykres. */ g_tree_traverse (drzewoDat, PokazDaneDnia, G_IN_ORDER, &nMaksDzien); /* --- Dane potrzebne do wyświetlenia wykresu --- */ daneWykresu.nMaksRozmiar = nMaksDzien; daneWykresu.kontrolka = clistaDni; daneWykresu.rzad = 0; /* --- Ponownie obchodzimy drzewo i wyświetlamy wykresy --- */ g_tree_traverse (drzewoDat, PokazWykres, G_IN_ORDER, &daneWykresu);
Drzewa, c-listy i zakładki
503
/* --- Teraz pokazujemy c-listę --- */ gtk_widget_show_all (GTK_WIDGET (clistaDni)); } /* * PokazWykresUzytk * * Wyświetla wykres dla każdego użytkownika. * Wywoływana podczas obchodu drzewa - jest to funkcja zwrotna, * operująca na danych z drzewa. * * wartosc - zawiera dane o działaniach tego użytkownika * dane - zawiera informacje o wykresie, w tym o kontrolce * i wartości maksymalnej. */ gint PokazWykresUzytk (gpointer klucz, gpointer wartosc, gpointer dane) { int ix; typDaneWykresu *daneWykresu = (typDaneWykresu *) dane; typStat *daneStat = (typStat *) wartosc; /* --- Jak duży powinien być wykres? --- */ ix = (long) (((double) daneStat->nRozmiar * LICZBA_WYKRESOW-1) / daneWykresu->nMaksRozmiar); /* --- Wybieramy piksmapę o odpowiednim rozmiarze --- */ gtk_clist_set_pixmap (GTK_CLIST (daneWykresu->kontrolka), daneWykresu->rzad, 3, piksmapyWykresow[ix], maski[ix]); /* --- Przechodzimy do następnego rzędu. --- */ daneWykresu->rzad++; return (0); } /* * WypelnijUzytk * * Wypełnia stronę notatnika danymi o ruchu według użytkowników. * Dokonujemy tego w dwóch krokach - najpierw wyświetlamy dane * tekstowe i obliczamy maksymalną wartość, a następnie rysujemy * wykres na podstawie maksymalnej wartości.
Część IV Rozbudowa GTK+
504 */ void WypelnijUzytk () { gchar *strWartosc[4]; gchar bufor0[88]; gchar bufor1[88]; gchar bufor2[88]; long nMaks; typDaneWykresu daneWykresu; /* --- Buforowane wartości --- */ strWartosc[0] = bufor0; strWartosc[1] = bufor1; strWartosc[2] = bufor2; strWartosc[3] = NULL;
/* --- Jeśli nie ma jeszcze c-listy użytkowników... --- */ if (clistaUzytk == NULL) { /* --- Tworzymy c-listę z nagłówkami --- */ clistaUzytk = gtk_clist_new_with_titles (4, szTytulyUzytk); /* --- Pokazujemy nagłówki --- */ gtk_clist_column_titles_show (GTK_CLIST (clistaUzytk)); /* --- Ustawiamy szerokość kolumn. --- */ gtk_clist_set_column_width (GTK_CLIST (clistaUzytk), 0, 80); gtk_clist_set_column_width (GTK_CLIST (clistaUzytk), 1, 80); gtk_clist_set_column_width (GTK_CLIST (clistaUzytk), 2, 80); /* --- Justujemy kolumny --- */ gtk_clist_set_column_justification (GTK_CLIST (clistaUzytk), 0, GTK_JUSTIFY_LEFT); gtk_clist_set_column_justification (GTK_CLIST (clistaUzytk), 1, GTK_JUSTIFY_RIGHT); gtk_clist_set_column_justification (GTK_CLIST (clistaUzytk), 2, GTK_JUSTIFY_RIGHT); /* --- Dodajemy c-listę do strony notatnika. --- */ gtk_container_add (GTK_CONTAINER (stronaUzytk), clistaUzytk); } /* --- Obchodzimy drzewo, aby wyświetlić wartości tekstowe
Drzewa, c-listy i zakładki
505
i znaleźć wartość maksymalną --- */ nMaks = 0; g_tree_traverse (drzewoUzytk, PokazDaneUzytk, G_IN_ORDER, &nMaks); /* -- Wypełniamy strukturę dla "graficznego" obchodu drzewa --- */ daneWykresu.nMaksRozmiar = nMaks; daneWykresu.kontrolka = clistaUzytk; daneWykresu.rzad = 0; /* --- Wyświetlamy wykresy --- */ g_tree_traverse (drzewoUzytk, &daneWykresu);
PokazWykresUzytk,
G_IN_ORDER,
gtk_widget_show_all (GTK_WIDGET (clistaUzytk)); }
Podsumowanie Teraz możemy korzystać z bardziej skomplikowanych kontrolek, aby tworzyć bardziej interesujące aplikacje. Kontrolka GtkTree wyświetla informacje w postaci przypominającej drzewo. Można oglądać wszystkie gałęzie drzewa, albo rozwinąć tylko te, które zawierają żądane informacje. Kontrolka GtkNotebook przydaje się do wyświetlania wielu stron, które mogą być przełączane przez użytkownika albo samą aplikację. Możemy ukryć zakładki notatnika i sterować wyświetlaniem stron z wnętrza programu. Kontrolka GtkClist znakomicie nadaje się do wyświetlania wielu kolumn danych, zazwyczaj pochodzących z bazy danych. W kontrolce można mieszać grafikę i tekst, co uprzyjemnia przeglądanie informacji.
Rozdział 15 Tworzenie własnych kontrolek W pewnym punkcie tworzenia aplikacji może się okazać, że istniejące kontrolki nie wystarczają: być może będziemy potrzebować kontrolki radaru w grze, albo program będzie tworzył wiele wykresów. GTK+ posiada bardzo obszerny zbiór kontrolek, ale nie mogą one zaspokoić wszystkich możliwych potrzeb. Na szczęście GTL+ udostępnia interfejs, który pozwala programistom rozszerzyć standardowy zbiór kontrolek.
Zrozumienie kontrolek Jednym z najlepszych sposobów na nauczenie się tworzenia nowych kontrolek jest lektura kodu źródłowego GTK+. Kod może okazać się znakomitą pomocą naukową, kiedy chcemy się dowiedzieć, jak coś zostało zrobione. W niniejszym rozdziale korzystamy z przykładów wziętych z GTK+, aby zademonstrować proces tworzenia kontrolek. Biblioteka GTK+ została zaprojektowana w sposób obiektowy. Ponieważ jednak zaimplementowano ją w C, a C nie jest językiem obiektowym, tworzenie kontrolek spełniających wymogi obiektowości wymaga pewnej wiedzy. Na szczęście, kiedy kontrolka jest już gotowa, używanie jej w ramach interfejsu obiektowego nie przysparza żadnych trudności – możemy mieć tylko pewne kłopoty ze zrozumieniem trików, do których trzeba się uciec podczas jej tworzenia. Każda kontrolka składa się z dwóch plików. Jeden zawiera właściwy kod, który tworzy i definiuje kontrolkę, a drugi jest plikiem nagłówkowym, definiującym struktury danych i prototypy funkcji używanych przez kontrolkę.
Dziedziczenie właściwości Kontrolki można oprzeć na już istniejących kontrolkach albo stworzyć od podstaw. Oparcie nowej kontrolki na innej ułatwia kodowanie, jeśli istniejąca kontrolka ma podobne właściwości. GtkToggleButton używa jako podstawy GtkButton i dziedziczy zachowania tej kontrolki, ponieważ w GtkButton zaimplementowano już wiele funkcji, które należałoby umie-
508
Część IV Rozbudowa GTK+
ścić w GtkToggleButton. Należy tylko dodać wszystkie właściwości specyficzne dla GtkToggleButton, aby przeciążyć właściwości kontrolki podstawowej.
Tworzenie kontrolek od podstaw Można także tworzyć nowe kontrolki od podstaw, choć wymaga to więcej pracy. Większość kontrolek tworzonych od podstaw opiera się na kontrolce GtkWidget, która oferuje pewną podstawową funkcjonalność, do której programista może dodać żądane cechy. Niektóre kontrolki wywodzą swoją podstawową funkcjonalność od kontrolki GtkMisc. W porównaniu z kontrolkami, które oparte są na GtkWidget, kontrolki wywodzące się od GtkMisc zużywają mniej zasobów, ponieważ nie posiadają związanego z nimi okna X Windows; rysowanie odbywa się w oknie ich kontrolki macierzystej. Ze względu na brak własnego okna kontrolki te mają jednak pewne ograniczenia. Na przykład kontrolka GtkLabel nie może otrzymywać zdarzeń związanych z myszą; aby etykieta otrzymywała takie zdarzenia, trzeba umieścić ją w kontrolce GtkEventBox. Etykiety wywiedziono od GtkMisc, ponieważ zazwyczaj służą tylko do wyświetlania informacji i nie muszą obsługiwać zdarzeń związanych z myszą.
Działanie kontrolek Kiedy tylko jest to możliwe, powinniśmy opierać nowe kontrolki na innych, ponieważ można w ten sposób wykorzystać pracę innych programistów i zmniejszyć rozmiary kodu. W tej części rozdziału przyjrzymy się istniejącej kontrolce i rozłożymy kod na części składowe, aby wyjaśnić działanie kontrolki. Większość zamieszczonych przykładów wzięto z kontrolki przycisku, ale kod wygląda podobnie we wszystkich kontrolkach.
Plik nagłówkowy Plik nagłówkowy kontrolki definiuje używane przez nią struktury danych oraz prototypy funkcji kontrolki. Ważną cechą plików nagłówkowych jest to, że nie mogą być dołączane wielokrotnie; w tym celu pliki nagłówkowe kontrolek korzystają z niepowtarzalnego identyfikatora, który wskazuje ich obecność. Co więcej, pliki nagłówkowe mogą być wykorzystane w kompilatorze C++, więc funkcje C powinny być odpo-
Tworzenie własnych kontrolek
509
wiednio oznaczone, aby zapewnić poprawne działanie konsolidatora. Plik gtkbutton.h zaczyna się w ten sposób: #ifndef __GTK_BUTTON_H__ #define __GTK_BUTTON_H__ #include #include #ifdef __cplusplus extern "C" { #endif /* __cplusplus */ /* * * tutaj znajduje się kod pliku nagłówkowego * */
Plik nagłówkowy powinien kończyć się dyrektywami, które odpowiadają tym z początku pliku: #ifdef __cplusplus } #endif /* __cplusplus */ #endif /* __GTK_BUTTON_H__ */
Oczywiście, tworząc nową kontrolkę, powinniśmy zmienić identyfikator GTK_BUTTON_H na własną etykietę.
Makra W pliku nagłówkowym definiujemy także makra, używane przez nową kontrolkę. Często wykorzystywanym makrem jest na przykład GTK_ BUTTON, które zamienia kontrolkę GtkWidget na kontrolkę GtkButton. To i inne makra są zdefiniowane poniżej. W przypadku kontrolki przycisku wyglądają one następująco: #define GTK_BUTTON(obj)
(GTK_CHECK_CAST ((obj),
➥GTK_TYPE_BUTTON, GtkButton)) #define GTK_BUTTON_CLASS(klass) ((klass),
(GTK_CHECK_CLASS_CAST
➥GTK_TYPE_BUTTON, GtkButtonClass)) #define GTK_IS_BUTTON(obj)
➥GTK_TYPE_BUTTON))
(GTK_CHECK_TYPE ((obj),
510
Część IV Rozbudowa GTK+
W tym przypadku makro GTK_TYPE_BUTTON jest zdefiniowane następująco: #define GTK_TYPE_BUTTON
(gtk_button_get_type ())
Makro GTK_BUTTON jest często używane w zamieszczonych w tej książce przykładowych programach, ale pozostałe są wykorzystywane głównie przez wewnętrzny kod kontrolki GtkButton.
Struktury danych Następnie trzeba określić potrzebne struktury danych. Musimy tutaj rozważyć, których sygnałów będziemy używać i jakie dane będziemy przechowywać wewnątrz struktur. W przypadku GtkButton struktura jest niewielka: struct _GtkButton { GtkContainer container; guint in_button : 1; guint button_down : 1; };
Struktura GtkButton definiuje lokalne dane, używane przez wszystkie przyciski. Pierwszym elementem struktury musi być kontrolka, od której wywodzi się nowa kontrolka. W tym przypadku jest to kontrolka GtkContainer. Pozostałe dane są wykorzystywane przez przycisk. Należy stworzyć także klasę przycisku. Podobnie jak w przypadku lokalnych danych i informacji dla kontrolki przycisku, klasa przycisku przechowuje dane wspólne dla wszystkich kontrolek określonego typu, na przykład dostępne sygnały. Klasa dla przycisku wygląda następująco: struct _GtkButtonClass { GtkContainerClass parent_class; void (* pressed) (GtkButton *button); void (* released) (GtkButton *button); void (* clicked) (GtkButton *button); void (* enter) (GtkButton *button); void (* leave) (GtkButton *button); };
Tworzenie własnych kontrolek
511
Zwróćmy uwagę, że tutaj także pierwszym elementem struktury jest klasa macierzysta, po której następuje lista wskaźników do funkcji. Struktura klasy macierzystej musi być zdefiniowana jako pierwsza w strukturze klasy kontrolki potomnej. Wskaźniki do funkcji są traktowane jak funkcje wirtualne i ustawiane podczas tworzenia kontrolki. Jeśli jednak inna kontrolka wykorzystuje GtkButton jako klasę podstawową (tak jest na przykład w przypadku GtkToggleButton), wówczas może ona zmodyfikować te funkcje, aby zmienić zachowanie przycisku. Musimy także użyć instrukcji typedef, aby zdefiniować nazwy struktur w postaci beż znaków podkreślenia: typedef struct _GtkButton GtkButton; typedef struct _GtkButtonClass GtkButtonClass;
Prototypy Ostatnia część nagłówka definiuje prototypy funkcji, używanych w aplikacjach. Prototypy powinny definiować przynajmniej funkcję new, oraz funkcje służące do manipulowania kontrolką. GtkButton posiada następujące funkcje: GtkType GtkWidget* GtkWidget* void void void void void
gtk_button_get_type gtk_button_new gtk_button_new_with_label gtk_button_pressed gtk_button_released gtk_button_clicked gtk_button_enter gtk_button_leave
(void); (void); (const gchar *label); (GtkButton *button); (GtkButton *button); (GtkButton *button); (GtkButton *button); (GtkButton *button);
Kod implementacyjny Plik C jest bardziej skomplikowany, niż nagłówek. Na szczęście większość kodu można skopiować (z pewnymi modyfikacjami) z istniejących kontrolek. Najpierw należy wyliczyć sygnały definiowane przez kontrolkę (nie umieszczamy tu sygnałów, które są już zdefiniowane w kontrolce bazowej). Kontrolka GtkButton definiuje następujące sygnały, po których musi występować znacznik LAST_SIGNAL: enum { PRESSED, RELEASED, CLICKED, ENTER,
512
Część IV Rozbudowa GTK+
LEAVE, LAST_SIGNAL };
Jednak przełącznik GtkToggleButton, który używa GtkButton jako klasy bazowej, musi zdefiniować tylko nowe sygnały, charakterystyczne dla przełącznika. Przełącznik dodaje tylko sygnał toggled, więc definiuje sygnały w następujący sposób: enum { TOGGLED, LAST_SIGNAL };
Każda kontrolka musi posiadać funkcję get_type, która dostarcza GTK+ informacji o kontrolce. Dane są umieszczane w strukturze GtkTypeInfo i przekazywane do funkcji gtk_type_unique, aby jednoznacznie zidentyfikować nową kontrolkę. W strukturze GtkTypeInfo należy umieścić następujące informacje: Nazwę kontrolki Rozmiar obiektu (na przykład, jak duża jest struktura GtkButton) Rozmiar klasy (na przykład, jak duża jest struktura GtkButtonClass) Funkcja inicjująca klasę Funkcja inicjująca obiekt Funkcja ustawiająca argument Funkcja pobierająca argument W przypadku przycisku struktura ta jest zdefiniowana następująco: GtkTypeInfo button_info = { "GtkButton", sizeof (GtkButton), sizeof (GtkButtonClass), (GtkClassInitFunc) gtk_button_class_init, (GtkObjectInitFunc) gtk_button_init, (GtkArgSetFunc) gtk_button_set_arg, (GtkArgGetFunc) NULL, };
Struktura ta jest przekazywana wraz z typem klasy macierzystej do funkcji gtk_type_unique, która generuje niepowtarzalny identyfikator kontrolki. Identyfikator ten należy wygenerować tylko raz, a zwracaną wartość
Tworzenie własnych kontrolek
513
zapamiętać. Będzie ona wykorzystywana za każdym razem, kiedy zostanie wywołana funkcja get_type dla przycisku. Cały kod dla funkcji get_type wygląda mniej więcej w ten sposób: GtkType gtk_button_get_type (void) { static GtkType button_type = 0; /* --- Jeśli nie wygenerowano jeszcze identyfikatora --- */ if (!button_type) { GtkTypeInfo button_info = { "GtkButton", sizeof (GtkButton), sizeof (GtkButtonClass), (GtkClassInitFunc) gtk_button_class_init, (GtkObjectInitFunc) gtk_button_init, (GtkArgSetFunc) gtk_button_set_arg, (GtkArgGetFunc) NULL, }; button_type = gtk_type_unique (gtk_container_get_type (), &button_info); } return button_type; }
Następnym krokiem jest zdefiniowanie funkcji gtk_button_class_init i gtk_button_init.
Inicjacja klasy Funkcję class_init, zdefiniowaną w funkcji get_type, wywołuje się po to, aby stworzyć strukturę klasy kontrolki. Struktura ta definiuje dane wspólne dla wszystkich kontrolek tego typu. Polega to na definiowaniu nowych sygnałów i redefiniowaniu (przeciążaniu) starych. Nowe sygnały dla przycisku dodaje się, definiując statyczną tablicę sygnałów w następujący sposób: static guint button_signals[LAST_SIGNAL] = 0;
514
Część IV Rozbudowa GTK+
LAST_SIGNAL zdefiniowano podczas wyliczania sygnałów. Tablicę trzeba będzie wypełnić identyfikatorami sygnałów kontrolki, generowanymi przez wywołanie funkcji gtk_signal_new. Inicjacja klasy przycisku składa się z kilku części. Najważniejsze fragmenty kodu opisujemy poniżej. Najpierw należy pobrać dane o klasie macierzystej ze struktury klasy kontrolki: GtkObjectClass *object_class; GtkWidgetClass *widget_class; GtkContainerClass *container_class; object_class = (GtkObjectClass*) klass; widget_class = (GtkWidgetClass*) klass; container_class = (GtkContainerClass*) klass; parent_class = gtk_type_class (gtk_container_get_type ());
Następnym krokiem jest utworzenie nowych sygnałów. Tablicę button_signals stworzono wcześniej, teraz należy wypełnić ją sygnałami. Każdy sygnał tworzy się przy pomocy funkcji gtk_signal_new i umieszcza pod indeksem tablicy, określonym przez wyliczenie. Funkcja gtk_signal_new jest zdefiniowana następująco: gint gtk_signal_new (const gchar *name, GtkSignalRunType run_type, GtkType object_type, gint function_offset, GtkSignalMarshaller marshaller, GtkType return_val, gint nparams, [parameter types]);
Sygnały pressed i clicked różnią się tylko nazwą i przesunięciem sygnału (function offset). Używają domyślnego „zawiadowcy” (marshaller) i nie mają parametrów: button_signals[PRESSED] = gtk_signal_new ("pressed", GTK_RUN_FIRST, object_class->type, GTK_SIGNAL_OFFSET (GtkButtonClass, pressed), gtk_signal_default_marshaller, GTK_TYPE_NONE, 0); button_signals[CLICKED] =
Tworzenie własnych kontrolek
515
gtk_signal_new ("clicked", GTK_RUN_FIRST, object_class->type, GTK_SIGNAL_OFFSET (GtkButtonClass, clicked), gtk_signal_default_marshaller, GTK_TYPE_NONE, 0);
Następnie należy dodać sygnały do klasy obiektu. gtk_object_class_add_signals (object_class, button_signals, LAST_SIGNAL);
Kontrolka może także przeciążyć dowolny sygnał z klasy macierzystej. Przycisk przeciąża wiele sygnałów kontrolki uniwersalnej i niektóre sygnały kontrolki pojemnika. widget_class->activate_signal = button_signals[CLICKED]; widget_class->map = gtk_button_map; widget_class->unmap = gtk_button_unmap; widget_class->realize = gtk_button_realize; widget_class->draw = gtk_button_draw; widget_class->draw_focus = gtk_button_draw_focus; widget_class->draw_default = gtk_button_draw_default; widget_class->size_request = gtk_button_size_request; widget_class->size_allocate = gtk_button_size_allocate; widget_class->expose_event = gtk_button_expose; widget_class->button_press_event = gtk_button_button_press; widget_class->button_release_event = gtk_button_button_release; widget_class->enter_notify_event = gtk_button_enter_notify; widget_class->leave_notify_event = gtk_button_leave_notify; widget_class->focus_in_event = gtk_button_focus_in; widget_class->focus_out_event = gtk_button_focus_out; container_class->add = gtk_button_add; container_class->remove = gtk_button_remove; container_class->foreach = gtk_button_foreach;
Następnie wypełniane są sygnały przycisku. Sygnał clicked jest ustawiany na NULL, ponieważ przycisk nie potrzebuje tego sygnału – chociaż mogą go potrzebować programiści, umieszczający przycisk w swoich aplikacjach. klass->pressed = gtk_real_button_pressed; klass->released = gtk_real_button_released; klass->clicked = NULL;
516
Część IV Rozbudowa GTK+
klass->enter = gtk_real_button_enter; klass->leave = gtk_real_button_leave;
Emitowanie sygnałów Sygnały w GTK+ można emitować przy pomocy funkcji gtk_signal_emit albo gtk_signal_emit_by_name. Zazwyczaj wewnątrz kodu kontrolki korzysta się z sygnału gtk_signal_emit, ponieważ kontrolka ma dostęp do tablicy sygnałów i zna identyfikator sygnału. Funkcja gtk_signal_emit_ by_name korzysta z nazwy, a nie identyfikatora sygnału i jest zwykle używana poza kontrolką. W kodzie GtkButton znajduje się funkcja gtk_button_clicked, która po wywołaniu emituje sygnał clicked. Czyni to, wywołując po prostu funkcję gtk_signal_emit: void gtk_button_clicked (GtkButton *button) { gtk_signal_emit (GTK_OBJECT (button), button_signals[CLICKED]); }
Kiedy chcemy spowodować wystąpienie zdarzenia, które można będzie obsłużyć w funkcji zwrotnej, możemy skorzystać z funkcji gtk_signal_ emit_by_name: gtk_signal_emit_by_name (GTK_OBJECT (przycisk), "changed");
Po emisji sygnał rozchodzi się do wszystkich procedur obsługi sygnału. Można zatrzymać propagację sygnału przy pomocy funkcji gtk_signal_ emit_stop_by_name, wywołując ją z którejś procedury obsługi. Zatrzymanie sygnału przydaje się wówczas, kiedy chcemy filtrować sygnały przechodzące przez kontrolkę. Możemy na przykład zabronić wpisywania niektórych znaków do kontrolki, pisząc funkcję zwrotną dla sygnału key_press_event, która wywołuje funkcję gtk_signal_emit_stop_ by_name, aby inne procedury obsługi nie otrzymywały sygnału key_press_event po naciśnięciu niektórych klawiszy.
Funkcja init Funkcja init inicjuje instancję przycisku. Polega to na zainicjowaniu danych w strukturze przycisku, a czasem także na stworzeniu innych kontrolek. Funkcja gtk_button_init jest bardzo prosta, ponieważ ustawia tylko kilka początkowych wartości: static void gtk_button_init (GtkButton *button) {
Tworzenie własnych kontrolek
517
GTK_WIDGET_SET_FLAGS (button, GTK_CAN_FOCUS); button->child = NULL; button->in_button = FALSE; button->button_down = FALSE; }
Inicjacja innych kontrolek może być bardziej skomplikowana. Kontrolka GtkFileSelection jest dużo bardziej złożona, ponieważ musi stworzyć wszystkie kontrolki potomne, znajdujące się w oknie dialogowym. Wewnątrz okna wyboru pliku umieszczane są między innymi pola listy i przyciski, co znacznie komplikuje inicjację kontrolki. Jeśli jednak tworzymy prostą kontrolkę, jej inicjacja będzie miała przebieg podobny do pokazanego wyżej.
Tworzenie kontrolki Po napisaniu kodu służącego do inicjacji struktur danych, klasy i samej kontrolki, ostatnią i bardzo ważną czynnością jest stworzenie funkcji, które pozwolą programistom korzystać z kontrolki. W przypadku GtkButton istnieją dwie funkcje, tworzące kontrolkę – gtk_button_new i gtk_button_new_with_label. Funkcja gtk_button_new tworzy kontrolkę pobierając jej typ i tworząc nową instancję tego typu. Jest to standardowy kod, służący do tworzenia kontrolki: GtkWidget* gtk_button_new (void) { return GTK_WIDGET (gtk_type_new (gtk_button_get_type ())); }
Bardziej interesująca jest funkcja, która tworzy przycisk z etykietą. Zauważmy, że wygląda ona zupełnie tak samo, jak normalne funkcje w programach GTK+. Kod tworzy przycisk i etykietę oraz umieszcza etykietę na przycisku – moglibyśmy uczynić to w aplikacji, nie znając wewnętrznych szczegółów implementacyjnych kontrolki. Korzysta on z wcześniej napisanych funkcji: GtkWidget* gtk_button_new_with_label (const gchar *label) { GtkWidget *button; GtkWidget *label_widget;
Część IV Rozbudowa GTK+
518
button = gtk_button_new (); label_widget = gtk_label_new (label); gtk_misc_set_alignment (GTK_MISC (label_widget), 0.5, 0.5); gtk_container_add (GTK_CONTAINER (button), label_widget); gtk_widget_show (label_widget); return button; }
Tworzenie kontrolki wykresu Tworzenie kontrolki od podstaw nie jest rzeczą trywialną, ale nie jest też szczególnie skomplikowane. Dostępność kodu źródłowego GTK+ jest ogromną zaletą, ponieważ zawarte w nim kontrolki stanowią nieocenione źródło wiedzy, którą możemy spożytkować do stworzenia własnych kontrolek. Aby zilustrować proces tworzenia kontrolki od podstaw, napiszemy kod kontrolki, która będzie mogła wyświetlać proste wykresy. Zabierając się do pisania nowej kontrolki warto jest zastanowić się, czy ma ona jakieś cechy zbliżone do już istniejących kontrolek. W przypadku kontrolki wykresu możemy oprzeć się na modelu kontrolki obszaru rysunkowego, która zawiera ograniczony zbiór funkcji pozwalających na rysowanie obiektów. Po przestudiowaniu kontrolki obszaru rysunkowego możemy twórczo rozwinąć zdobytą wiedzę, przystępując do pisania kontrolki wykresu.
Rysunek 15.1. Kontrolka wykresu.
Plik nagłówkowy Niewielki plik nagłówkowy kontrolki wykresu zawiera niezbędne minimum definicji, które pozwolą na jej funkcjonowanie. Kontrolka przechowuje dane wykresu w tablicy values, zawartej w strukturze danych kon-
Tworzenie własnych kontrolek
519
trolki. Struktura ta posiada również pole przechowujące liczbę elementów wykresu. /* * Plik: gtkgraph.h * Autor: Eric Harlow */ #ifndef __GTK_GRAPH_H__ #define __GTK_GRAPH_H__ #include #include #ifdef __cplusplus extern "C" { #endif /* __cplusplus */ /* * --- Makra służące do konwersji i sprawdzania typów */ #define GTK_GRAPH(obj) \ GTK_CHECK_CAST (obj, gtk_graph_get_type (), GtkGraph) #define GTK_GRAPH_CLASS(klass) \ GTK_CHECK_CLASS_CAST (klass, gtk_graph_get_type, Class) #define GTK_IS_GRAPH(obj) \ GTK_CHECK_TYPE (obj, gtk_graph_get_type ()) /* * --- Definiowane struktury danych */ typedef struct _GtkGraph GtkGraph; typedef struct _GtkGraphClass GtkGraphClass; /* * Dane wykresu. */ struct _GtkGraph { GtkWidget vbox; gint *values; gint num_values;
GtkGraph-
520
Część IV Rozbudowa GTK+
}; /* * Dane klasy wykresu. */ struct _GtkGraphClass { GtkWidgetClass parent_class; }; /* * Prototypy funkcji */ GtkWidget* gtk_graph_new (void); void gtk_graph_size (GtkGraph *graph, int size); void gtk_graph_set_value (GtkGraph *graph, int index, int value); #ifdef __cplusplus } #endif /* __cplusplus */ #endif /* __GTK_GRAPH_H__ */
Kod kontrolki wykresu Kod wykresu definiuje kontrolkę jako wywodzącą się od GtkWidget. Kontrolka wykresu musi przeciążyć funkcje kontrolki uniwersalnej draw, expose, realize i size_request, aby przejąć kontrolę nad swoim zachowaniem. GtkGraph przeciąża także funkcję destroy, aby zwolnić zajmowaną pamięć, zanim wywoła funkcję zwrotną destroy z klasy macierzystej. Funkcje sterujące zawartością wykresu to gtk_graph_size, która ustawia liczbę słupków wykresu, oraz gtk_graph_set_value, która ustawia wysokość poszczególnych słupków. Zasadnicza część kodu znajduje się w funkcji draw. Funkcja draw dla kontrolki wykresu nosi nazwę gtk_graph_draw. Będzie ona w razie potrzeby wyświetlać wykres. Oto pełny kod: /* * Plik: GtkGraph.c * Autor: Eric Harlow * * Prosta kontrolka wykresu */
Tworzenie własnych kontrolek
521
#include #include #include #include #include "gtkgraph.h" static GtkWidgetClass *parent_class = NULL; /* * deklaracje prototypów: */ static void gtk_graph_class_init (GtkGraphClass *class); static void gtk_graph_init (GtkGraph *graph); static void gtk_graph_realize (GtkWidget *widget); static void gtk_graph_draw (GtkWidget *widget, GdkRectangle *area); static void gtk_graph_size_request (GtkWidget *widget, GtkRequisition *req); static gint gtk_graph_expose (GtkWidget *widget, GdkEventExpose *event); static void gtk_graph_destroy (GtkObject *object); /* * gtk_graph_get_type * * Klasa wewnętrzna. Definiuje klasę GtkGraph na potrzeby GTK. */ guint gtk_graph_get_type (void) { static guint graph_type = 0; /* --- Jeśli typ jeszcze nie został utworzony --- */ if (!graph_type) { /* --- Tworzymy obiekt graph_info --- */ GtkTypeInfo graph_info = { "GtkGraph", sizeof (GtkGraph), sizeof (GtkGraphClass), (GtkClassInitFunc) gtk_graph_class_init, (GtkObjectInitFunc) gtk_graph_init, (GtkArgSetFunc) NULL, (GtkArgGetFunc) NULL,
Część IV Rozbudowa GTK+
522 };
/* --- Rejestrujemy go w GTK - pobieramy unikalny identyfikator --- */ graph_type = gtk_type_unique (gtk_widget_get_type (), &graph_info); } return graph_type; } /* * gtk_graph_class_init * * Przeciążamy metody dla kontrolki uniwersalnej, aby klasa * kontrolki wykresu funkcjonowała prawidłowo. Tutaj * redefiniujemy funkcje, które powodują przerysowanie * kontrolki. * * class - klasa definicji obiektu. */ static void gtk_graph_class_init (GtkGraphClass *class) { GtkObjectClass *object_class; GtkWidgetClass *widget_class; /* --- Pobieramy klasę kontrolki --- */ object_class = (GtkObjectClass *) class; widget_class = (GtkWidgetClass *) class; parent_class = gtk_type_class (gtk_widget_get_type ()); /* --- Przeciążamy usuwanie obiektu --- */ object_class->destroy = gtk_graph_destroy; /* --- Przeciążamy następujące metody: --- */ widget_class->realize = gtk_graph_realize; widget_class->draw = gtk_graph_draw; widget_class->size_request = gtk_graph_size_request; widget_class->expose_event = gtk_graph_expose; } /* * gtk_graph_init * * Wywoływana za każdym razem, kiedy tworzony jest * element GtkGraph. Inicjuje pola w naszej
Tworzenie własnych kontrolek
* strukturze. */ static void gtk_graph_init (GtkGraph *graph) { GtkWidget *widget; widget = (GtkWidget *) graph; /* --- Wartości początkowe --- */ graph->values = NULL; graph->num_values = 0; } /* * gtk_graph_new * * Tworzy nowy element GtkGraph */ GtkWidget* gtk_graph_new (void) { return gtk_type_new (gtk_graph_get_type ()); } /* * gtk_graph_realize * * Wiąże kontrolkę z oknem X Windows * */ static void gtk_graph_realize (GtkWidget *widget) { GtkGraph *darea; GdkWindowAttr attributes; gint attributes_mask; /* --- Sprawdzamy błedy --- */ g_return_if_fail (widget != NULL); g_return_if_fail (GTK_IS_GRAPH (widget)); darea = GTK_GRAPH (widget); GTK_WIDGET_SET_FLAGS (widget, GTK_REALIZED); /* --- atrybuty tworzonego okna --- */
523
Część IV Rozbudowa GTK+
524
attributes.window_type = GDK_WINDOW_CHILD; attributes.x = widget->allocation.x; attributes.y = widget->allocation.y; attributes.width = widget->allocation.width; attributes.height = widget->allocation.height; attributes.wclass = GDK_INPUT_OUTPUT; attributes.visual = gtk_widget_get_visual (widget); attributes.colormap = gtk_widget_get_colormap (widget); attributes.event_mask = gtk_widget_get_events (widget) | GDK_EXPOSURE_MASK; /* --- Przekazujemy x, y, wartości wizualne i mapę kolorów --- */ attributes_mask = GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL | GDK_WA_COLORMAP; /* --- Tworzymy okno --- */ widget->window = gdk_window_new ( gtk_widget_get_parent_window (widget), &attributes, attributes_mask); gdk_window_set_user_data (widget->window, darea); widget->style = gtk_style_attach (widget->style, widget->window); gtk_style_set_background (widget->style, widget->window, GTK_STATE_NORMAL); } /* * gtk_graph_size * * Metoda ustawiająca rozmiar wykresu. */ void gtk_graph_size (GtkGraph *graph, int size) { g_return_if_fail (graph != NULL); g_return_if_fail (GTK_IS_GRAPH (graph)); graph->num_values = size; graph->values = g_realloc (graph->values, sizeof (gint) * size); } /* * gtk_graph_set_value *
Tworzenie własnych kontrolek
* Metoda ustawiająca poszczególne wartości wykresu. */ void gtk_graph_set_value (GtkGraph *graph, int index, int value) { g_return_if_fail (graph != NULL); g_return_if_fail (GTK_IS_GRAPH (graph)); g_return_if_fail (index < graph->num_values && index >= 0); graph->values[index] = value; } /* * gtk_graph_draw * * Rysowanie kontrolki. */ static void gtk_graph_draw (GtkWidget *widget, GdkRectangle *area) { GtkGraph *graph; int width; int height; int column_width; int max = 0; int i; int bar_height; /* --- Sprawdzamy oczywiste błędy --- */ g_return_if_fail (widget != NULL); g_return_if_fail (GTK_IS_GRAPH (widget)); /* --- Upewniamy się, że kontrolkę można narysować --- */ if (GTK_WIDGET_DRAWABLE (widget)) { graph = GTK_GRAPH (widget); if (graph->num_values == 0) { return; } /* --- Pobieramy szerokość i wysokość --- */ width = widget->allocation.width - 1; height = widget->allocation.height - 1; /* --- Obliczamy szerokość kolumn --- */
525
Część IV Rozbudowa GTK+
526 column_width = width / graph->num_values; /* --- Znajdujemy wartość maksymalną --- */ for (i = 0; i < graph->num_values; i++) { if (max < graph->values[i]) { max = graph->values[i]; } } /* --- Wyświetlamy każdy słupek wykresu --- */ for (i = 0; i < graph->num_values; i++) {
bar_height = (graph->values[i] * height) / max; gdk_draw_rectangle (widget->window, widget->style->fg_gc[GTK_STATE_NORMAL], TRUE, (i * column_width), height-bar_height, (column_width-2), bar_height); } } } /* * gtk_graph_size_request * * Jak duża powinna być kontrolka? * Wartości te można zmodyfikować. */ static void gtk_graph_size_request (GtkWidget *widget, GtkRequisition *req) { req->width = 200; req->height = 200; } /* * gtk_graph_expose * * Kontrolka wykresu została odsłonięta i trzeba
Tworzenie własnych kontrolek
* ją przerysować * */ static gint gtk_graph_expose (GtkWidget *widget, GdkEventExpose *event) { GtkGraph *graph; /* --- Sprawdzamy błędy --- */ g_return_val_if_fail (widget != NULL, FALSE); g_return_val_if_fail (GTK_IS_GRAPH (widget), FALSE); g_return_val_if_fail (event != NULL, FALSE); if (event->count > 0) { return (FALSE); } /* --- Pobieramy kontrolkę wykresu --- */ graph = GTK_GRAPH (widget); /* --- Czyścimy okno --- */ gdk_window_clear_area (widget->window, 0, 0, widget->allocation.width, widget->allocation.height); /* --- Rysujemy wykres --- */ gtk_graph_draw (widget, NULL); } static void gtk_graph_destroy (GtkObject *object) { GtkGraph *graph; /* --- Sprawdzamy typ --- */ g_return_if_fail (object != NULL); g_return_if_fail (GTK_IS_GRAPH (object)); /* --- Przekształcamy na obiekt wykresu --- */ graph = GTK_GRAPH (object); /* --- Zwalniamy pamięć --- */ g_free (graph->values);
527
Część IV Rozbudowa GTK+
528
/* --- Wywołujemy macierzystą funkcję "destroy" --- */ GTK_OBJECT_CLASS (parent_class)->destroy (object); }
Korzystanie z kontrolki Możemy teraz wykorzystać kontrolkę do wyświetlenia prostego wykresu w oknie aplikacji. Poniższy przykładowy kod tworzy kontrolkę wykresu i wypełnia ją przeznaczonymi do wyświetlenia wartościami. /* * Plik: main.c * Autor: Eric Harlow * * Przykład użycia własnej kontrolki. */ #include #include "gtkgraph.h" /* * ZamknijOknoApl * * Okno się zamyka, kończymy pętlę GTK. */ gint ZamknijOknoApl (GtkWidget *kontrolka, gpointer *dane) { gtk_main_quit (); return (FALSE); } /* * main - program zaczyna się tutaj */ int main (int argc, char *argv[]) { GtkWidget *okno; GtkWidget *wykres; /* --- Inicjacja GTK --- */ gtk_init (&argc, &argv);
Tworzenie własnych kontrolek
/* --- Tworzymy okno najwyższego poziomu --- */ okno = gtk_window_new (GTK_WINDOW_TOPLEVEL); gtk_window_set_title (GTK_WINDOW (okno), "Wykres słupkowy"); /* --- Należy zawsze podłączyć sygnał delete_event do głównego okna. --- */ gtk_signal_connect (GTK_OBJECT (okno), "delete_event", GTK_SIGNAL_FUNC (ZamknijOknoApl), NULL); /* --- Ustawiamy obramowanie okna --- */ gtk_container_border_width (GTK_CONTAINER (okno), 20); /* * --- Tworzymy wykres */ /* --- Tworzymy nowy wykres. --- */ wykres = gtk_graph_new (); /* --- Pokazujemy wykres --- */ gtk_widget_show (wykres); /* --- Ustawiamy liczbę elementów wykresu --- */ gtk_graph_size (GTK_GRAPH (wykres), 5); /* --- Ustawiamy wysokość wszystkich elementów --- */ gtk_graph_set_value (GTK_GRAPH (wykres), 0, 5); gtk_graph_set_value (GTK_GRAPH (wykres), 1, 10); gtk_graph_set_value (GTK_GRAPH (wykres), 2, 15); gtk_graph_set_value (GTK_GRAPH (wykres), 3, 20); gtk_graph_set_value (GTK_GRAPH (wykres), 4, 25); gtk_widget_draw (wykres, NULL); /* * --- Uwidaczniamy główne okno */ gtk_container_add (GTK_CONTAINER (okno), wykres); gtk_widget_show (okno); gtk_main (); exit (0); }
529
530
Część IV Rozbudowa GTK+
Podsumowanie Tworzenie kontrolek jest łatwe. Kontrolki udostępniają aplikacjom dużo prostszy interfejs, niż sugeruje to ich wewnętrzny kod. Tworzone kontrolki mogą dziedziczyć wiele właściwości po istniejących kontrolkach, mogą też być tworzone od podstaw. Tworzenie kontrolki od podstaw wymaga więcej pracy i kodowania, ale jest bardziej elastyczne.