Rozdział 9. Budowanie GUI za pomocą Glade i GTK+/GNOME
Omówimy teraz Glade, czyli wydajne narzędzie przeznaczone do szybkiego projektowania graficznych interfejsów użytkownika w środowisku GNOME/GTK. W tym rozdziale dokonamy przeglądu różnych właściwości Glade i różnych sposobów jego wykorzystania, a także pokażemy krok po kroku sposób tworzenia GUI dla naszej aplikacji obsługującej wypożyczalnię płyt DVD. Przy okazji zobaczymy także wszystkie właściwości GNOME, GTK+ i glib, o których wspominaliśmy w poprzednim rozdziale i które teraz będą użyte w konkretnym programie.
Przed rozpoczęciem pracy na dobre chcemy zwrócić uwagę, że borykanie się ze złożonością problemów spotykanych przy tworzeniu rzeczywistego GUI jest wielkim wyzwaniem dla niedoświadczonego programisty. Będziemy tworzyć stosunkowo złożony interfejs o nie okrojonych możliwościach — a więc jego kod jest skomplikowany i całe zadanie realizowane w celach pokazowych jest dość ambitne.
Mamy zamiar doprowadzić do tego, aby Czytelnik sam się przekonał, jak ważne jest pamiętanie o elemencie najmniej przewidywalnym, czyli o przyszłym użytkowniku. Oczekuje on, że interfejs graficzny będzie wykonywał wszystko w wymagany sposób, spójnie i bezpiecznie. Tworząc poważny program, pokażemy najpierw niektóre drobniejsze problemy, na które natknie się niedoświadczony programista GUI.
Ten rozdział ma następującą strukturę:
Ogólne omówienie Glade.
Samouczek Glade.
libglade.
Tworzenie interfejsu naszej przykładowej aplikacji w środowisku GNOME.
Ogólne omówienie Glade
Glade służy do tworzenia interfejsów użytkownika w systemie Linux. Narzędzie to pozwala programiście zaprojektować układ okien, okienek dialogowych, menu i pasków narzędzi. Podczas pracy szeroko wykorzystuje się myszkę, podobnie jak w programie malarskim. Analogia do takiego programu jest całkowicie uzasadniona, ponieważ znaczenie ikon i innych elementów jest dobrze znane doświadczonym projektantom grafiki korzystającym z takich narzędzi jak Gimp. Programy takie jak Glade pozwalają na tworzenie interfejsu użytkownika przez projektantów bez zwracania większej uwagi na podstawy działania programu, który ma być obsługiwany przez ten interfejs — bowiem projekt interfejsu jest oddzielony od kodu właściwego programu.
Program Glade jest najbardziej zaawansowanym narzędziem z grupy programów do szybkiego tworzenia aplikacji (RAD) w środowisku GNOME/GTK+ i jest podobny do narzędzi spotykanych w Windows, takich jak Power Builder i Visual C++ Resource Editor. Po rozpoczęciu projektowania Glade może utworzyć szkielet kodu źródłowego, łącznie z funkcjami tworzącymi okna i okienka dialogowe.
Jako alternatywę tego narzędzia możemy w graficznym interfejsie naszej aplikacji użyć biblioteki libglade, która służy do załadowania GUI w formacie XML podczas pracy programu. Takie dynamiczne ładowanie interfejsu jest bardzo zaawansowaną właściwością, dzięki której można korzystać z różnych interfejsów zależnie od okoliczności, lecz tym zajmiemy się nieco później.
Taka koncepcja jest faktycznie znacznie szersza niż zmiana „skór” znana z programu Winamp. Użycie „skór” pozwala na modyfikację wyglądu interfejsu i podobnie jak zmiana motywu w menedżerze okien X Window, służy tylko zmianom estetycznym. Biblioteka libglade umożliwia także modyfikację funkcjonalną interfejsu, dzięki czemu można uzyskać o wiele większą elastyczność.
Glade można używać do projektowania GUI, a także jako doskonałego narzędzia dydaktycznego dla GNOME/GTK+. Dzięki Glade mamy natychmiastowy dostęp do wszystkich popularnych widżetów GNOME/GTK+, łącznie z ich właściwościami, które można dowolnie kształtować, widząc natychmiast wyniki swojej pracy.
Uwagi na temat projektowania GUI
Powodzenie aplikacji zależy w znacznej mierze od jakości użytego interfejsu użytkownika i nie jest to rewolucyjne spostrzeżenie. Jeżeli interfejs wydaje się skomplikowany, mylący lub zgoła dziwny dla nowego użytkownika, wówczas cały produkt nie robi żadnego wrażenia, niezależnie od jakości swoich pozostałych elementów. Dobry interfejs użytkownika nie może więc być utworzony za pomocą prostych metod.
Na ten temat napisano wiele książek zawierających recepty dotyczące projektowania GUI, ale nietrudno wskazać popularne aplikacje, które nie są zgodne z wieloma tzw. „zasadami”. Rozważmy np. przyciski na pasku narzędziowym, których pierwotnym zadaniem było dublowanie często używanych pozycji menu w celu uzyskania do nich szybszego dostępu. Teraz spójrzmy, co występuje w programie Internet Explorer 5: po naciśnięciu przycisku Mail na pasku narzędziowym rozwija się menu podrzędne! Można to traktować jako niewygodę lub ulepszenie, ale niezależny od odczuć pozostaje oczywisty fakt: niespójny interfejs może wprowadzać użytkownika w błąd. Jeśli użytkownik oczekuje, że pewne funkcje będą działać w określony sposób, to przed próbą zmiany tego działania należy dobrze się zastanowić!
[[[ramka]]]
W akceptowalnych przez użytkowników projektach GUI zachodzą zmiany podobne do zmian w użyciu słów, w gramatyce i pisowni. Jeżeli byłoby trzeba podać tu podstawową zasadę, to będzie nią utrzymanie podobieństwa do interfejsu innych programów i pamiętanie o kluczowych modnych określeniach: intuicyjny, przyjazny dla użytkownika i prosty w użyciu.
[koniec ramki]]]
Samouczek Glade
W tym podrozdziale zapoznamy się z właściwościami Glade, tworząc przykładową aplikację o nazwie Example. Aplikacja ta umożliwi wprowadzenie tekstu do widżetu GtkEntry i wyświetlenie go w postaci komunikatu w okienku dialogowym. Podany tu materiał zawiera:
omówienie edycyjnych funkcji Glade,
opis tworzenia przez Glade prototypowego kodu źródłowego,
opis połączenia projektu utworzonego za pomocą Glade z kodem odczytującym tekst i tworzącym okienko dialogowe.
Glade wchodzi w skład pakietów tworzących GNOME, a więc zapewne każdy ma ten program w swojej dystrybucji Linuksa. Podobnie jak większość oprogramowania GNOME/GTK+, status Glade w momencie pisania tej książki był określany jako „stabilny, lecz nie ukończony” (była to wersja 0.5.7). Dlatego właśnie warto sprawdzić najnowszą wersję (jest ona dostępna pod adresem glade.pn.org), zawierającą więcej właściwości i z poprawionymi błędami. Na stronie WWW projektu Glade można także zapisać się na pocztową listę dyskusyjną oraz pobrać stamąd najnowsze wersje dokumentacji i zapoznać się z istniejącymi programami utworzonymi za pomocą tego narzędzia.
Zakładając, że mamy poprawnie zainstalowany program Glade, możemy wywołać go z menu panela GNOME, ponieważ podczas instalacji tworzony jest wpis w sekcji Development. Alternatywny sposób uruchomienia polega na użyciu polecenia glade w oknie terminala.
|
Glade po uruchomieniu wyświetla trzy okna: okno główne projektu (ang. main project window), paletę z widżetami (ang. widget palette) i edytor właściwości (ang. properties editor).
Okno główne
Okno główne (początkowo puste) zawiera listę okien, okienek dialogowych i menu związanych z bieżącym projektem. Nowy projekt tworzymy, korzystając z menu File | New Project, a następnie otwierając okno dialogowe Project Options za pomocą kliknięcia na odpowiedni przycisk paska narzędziowego lub z menu File | Project Options.
|
Na zakładce General można wpisać nazwę projektu i katalogu oraz nazwę pliku, który będzie używany przez Glade do przechowywania reprezentacji XML naszego projektu. Grupa przycisków opcji (ang. radio buttons) pozwala na wybór języka programowania używanego przez Glade do zapisu kodu źródłowego. Mamy także do dyspozycji przycisk opcji (ang. toggle button) włączający wspomaganie GNOME. Jeśli jest on wyłączony, to do dyspozycji jest tylko GTK+.
|
Na zakładkach C Options i libglade umieszczono opcje szczegółowe opisujące zachowywanie kodu źródłowego w języku C tworzonego przez Glade oraz opcje biblioteki libglade.
|
Paleta
Okno Palette zawiera widżety, które mogą być użyte w budowanym interfejsie. Jest ono podzielone na trzy zakładki o nazwach GTK+ Basic, GTK+ Additional i Gnome.
Zakładka GTK+ Basic zawiera najczęściej stosowane widżety GTK+, takie jak etykiety (ang. labels), okna edycyjne (ang. entry boxes), ramki (ang. frames) i elementy zbiorcze (ang. packing widgets).
|
Zakładka GTK+ Additional zawiera bardziej specjalistyczne widżety GTK+, takie jak skale (ang. scale) i linijki pomiarowe (ang. ruler).
|
Zakładka GNOME zawiera wszystkie widżety GNOME obsługiwane przez Glade.
|
Na tych zakładkach przedstawiono całe mnóstwo widżetów, ale niektóre z nich trudno zidentyfikować na podstawie ich ikon. Aby ograniczyć możliwość pomyłek, po najechaniu myszą na ikonę wyświetlane są krótkie podpowiedzi.
Sposób wyboru elementu z palety zależy od tego, czy wybierany jest widżet najwyższego poziomu (GtkWindow, GnomeApp, GtkDialog i GnomeDialog), czy też inny (np. GtkLabel lub GtkButton). Wynika to stąd, że etykiety i przyciski mogą być umieszczane tylko wewnątrz widżetów najwyższego poziomu. Wybór widżetu będącego oknem tworzy nowy egzemplarz (ang. instance) tego widżetu, a następnie powoduje dodanie swojego odnośnika (ang. reference) na liście w oknie głównym.
Zwracamy uwagę na możliwości pomyłek w użyciu określeń „okno”, „okienko dialogowe”, „GtkWindow”, „GtkDialog” itd. — w szczególności spowodowanych tym, że np. GtkDialog jest oknem, lecz nie można go określać jako GtkWindow (pomimo że jest jego elementem pochodnym).
Dla ułatwienia, w odniesieniu do elementów GtkWindow, GnomeApp, GtkDialog i GnomeDialog będziemy się posługiwać jednym określeniem „okno” wszędzie tam, gdzie rozróżnianie okna i okienka dialogowego nie jest istotne, zaś nazwy widżetów będziemy stosowali tam, gdzie to rozróżnienie jest potrzebne.
Po podwójnym kliknięciu na jakąś pozycję w oknie głównym Glade wyświetla jej okno, które możemy zabudowywać różnymi widżetami. Zamknięcie okna lub okienka dialogowego polega tylko na ukryciu go; po podwójnym kliknięciu jego nazwy zostanie ono ponownie wyświetlone. W rzeczywistości jedynym sposobem trwałego usunięcia pozycji z listy okien jest jej zaznaczenie i użycie menu Edit | Cut w oknie głównym.
Dodajmy więc nowe okno. Na zakładce Gnome należy w tym celu zaznaczyć ikonę GnomeApp, co zaowocuje natychmiastowym utworzeniem widżetu GnomeApp. Zwróćmy uwagę, że od razu są w nim widoczne menu, pasek narzędziowy i pasek statusu, bowiem Glade tworzy te elementy automatycznie jako części widżetu GnomeApp.
Jedną z dość niezwykłych właściwości Glade jest to, że każdy obszar, na którym można umieszczać widżet potomny (ang. child widget), a więc główna część widżetu GnomeApp lub komórka w GtkTable, jest zakreskowany, dopóki nie umieścimy na nim jakiegoś widżetu potomnego. Można tu wstawiać dowolne widżety z wyjątkiem okien, zaznaczając odpowiednią ikonę na palecie i klikając następnie zakreskowany obszar.
|
Spróbujmy poćwiczyć wstawianie wszystkich widżetów, zwracając uwagę na zmiany zachodzące w oknie Properties. Pokazywane tam opcje dotyczą wybranego w danym momencie widżetu. Kliknięcie widżetu prawym klawiszem myszy otwiera dostęp do zwykłych funkcji edycyjnych typu „wytnij i wstaw”.
Okno właściwości
Okno Properties jest najważniejszym oknem w programie Glade. Jest to miejsce, w którym są definiowane takie właściwości poszczególnych widżetów, jak nazwa, szerokość obrzeża, rozmiary oraz sygnały przyspieszające (ang. accelerators) i używane przy połączeniach (ang. connecting signals).
|
Pola okna Properties zależą od kontekstu — oznacza to, że zmieniają się one zgodnie z aktualnie wybranym widżetem. Modyfikując właściwości za pomocą Glade, można się wiele dowiedzieć na temat samych widżetów.
Dodajmy na przykład czwarty przycisk do paska narzędziowego w GnomeApp i skojarzmy reprezentowane przez niego wywołanie funkcji z kliknięciem myszą. W tym celu wybieramy pasek narzędziowy i wywołujemy zakładkę Widget w oknie edytora właściwości. Pole size określa liczbę widżetów na pasku narzędziowym, a więc, zwiększając jego zawartość o jeden, dodajemy miejsce na czwarty przycisk.
|
Na pasku narzędziowym pojawi się teraz mały zakreskowany obszar. W oknie Palette wybieramy teraz zakładkę Basic, a z niej widżet przycisku (oznaczony ikoną z napisem „OK”, można także skorzystać z kontekstowych podpowiedzi) i następnie klikamy zakreskowany obszar na pasku narzędziowym, wstawiając tam przycisk. Wybrawszy ten nowy element, zmieńmy jego nazwę na „quit_button” i etykietę na „Quit”. Wybierzmy też ikonę „quit” z rozwijanej listy (ang. icon combo box) w edytorze właściwości. Zwróćmy uwagę na zmiany etykiety i ikony, zachodzące w oknie GnomeApp.
|
Następnie wybierzmy zakładkę Place i zmieńmy tam opcję New Group na Yes. Spowoduje to dodanie pionowego separatora na lewo od nowej ikony. Otwórzmy teraz zakładkę Signal i dodajmy sprzężenie (ang. callback) do sygnału kliknięcia w następujący sposób:
Kliknąć przycisk z wielokropkiem (ang. ellipsis button) umieszczony z prawej strony pola edycyjnego przeznaczonego na nazwę sygnału, co spowoduje otwarcie listy sygnałów dostępnych dla widżetu GtkButton.
Wybrać sygnał „clicked” i nacisnąć przycisk OK.
Kliknąć przycisk Add, aby dodać do listy wybraną funkcję wywołania zwrotnego.
|
Glade automatycznie wypełnia pole edycyjne Handler, wpisując tam nazwę domyślnej funkcji wywołania zwrotnego. Ma ona postać „on_<nazwa_widżetu>_<sygnał>”. Ponieważ nasz nowy przycisk ma nazwę quit_button, Glade użyje nazwy funkcji on_quit_button_clicked. Widzimy teraz, jak ważna jest zmiana domyślnej nazwy nowego widżetu, ponieważ łatwiej jest wówczas rozpoznać nazwę powiązanej z nim funkcji.
Dodajmy teraz jakąś zawartość do głównego obszaru okna GnomeApp. Niech będą to widżety ramki i prostokąta zbiorczego (ang. vertical box widget; obydwa wybieramy z zakładki Basic w oknie Palette). Glade żąda podania rozmiaru prostokąta zbiorczego w postaci liczby wierszy — wybierzmy więc wysokość równą wysokości dwóch wierszy. Umieścimy tam przycisk i tekstowe pole edycyjne (ang. text entry widget).
Możemy teraz dostroić naszą konstrukcję. Wybierając w głównym oknie menu View | Show Widget Tree, wyświetlamy drzewiastą strukturę widżetów.
To okno dialogowe jest bardzo przydatne podczas wprowadzania ogólnych modyfikacji, ponieważ można rozwinąć w nim drzewo widżetów i wyświetlić relacje między wszystkimi widżetami potomnymi w projekcie. Zaznaczenie widżetu w oknie Widget Tree automatycznie powoduje jego zaznaczenie także w otwartym oknie oraz w oknie Properties, co znacznie upraszcza śledzenie zachowania się widżetów, szczególnie gdy przy ich większej liczbie nie wszystkie są widoczne.
Rozwińmy teraz dock1, aby zobaczyć trzy zawarte w nim elementy widżetu GnomeApp, czyli dodane przed chwilą pasek menu, pasek narzędziowy i widżet ramki. Wybierzmy ramkę i zmodyfikujmy jej właściwości tak, aby uzyskać jak najlepszy wygląd. Można np. dodać etykietę i powiększyć szerokość obrzeża.
|
Na zakończenie zmodyfikujemy właściwości widżetów przycisku i tekstowego pola edycyjnego.
Po wybraniu przycisku zmienimy jego nazwę („click_me_button”), etykietę („Click Me”) i wypełnienie oraz dodamy tak jak poprzednio funkcję wywołania zwrotnego powiązaną z sygnałem kliknięcia. Tekstowe pole edycyjne nazwiemy „text_entry” i dodamy domyślny tekst „Enter Some Text!”.
Zakończyliśmy w ten sposób projektowanie interfejsu użytkownika dla naszej podstawowej aplikacji Glade. Zachowujemy projekt pod nazwą Example, korzystając z przycisku Save na pasku narzędziowym okna głównego. Zanim przejdziemy dalej i zaczniemy omawiać tworzenie kodu, omówimy teraz niektóre szczegóły projektu tworzonego za pomocą Glade.
Kod źródłowy utworzony za pomocą Glade
Podczas budowania projektu z wykorzystaniem języka C, Glade tworzy i automatycznie zachowuje w odpowiednim katalogu pełne drzewo kodu źródłowego tego projektu. Korzystając z okna dialogowego Project Options, możemy podać ścieżkę do katalogu z projektem, nazwę projektu oraz nazwy różnych plików .c wchodzących w jego skład. Zazwyczaj trzeba tylko użyć jakiejś sensownej nazwy projektu, a wartości domyślne pozostałych jego elementów są na ogół całkowicie wystarczające.
Musimy tu jednak pamiętać, że trzeba:
znać format generowanego kodu źródłowego,
znać miejsce, w którym Glade przechowuje ten kod,
znać sposób manipulacji widżetami utworzonymi przez Glade,
wiedzieć, czy bezpieczne jest dodanie własnego kodu bez ryzyka zastąpienia go przez Glade, jeżeli chcemy zmodyfikować projekt i przebudować interfejs.
Utwórzmy więc projekt i spróbujmy to przećwiczyć. Najpierw zmienimy ustawienia, korzystając z okna Project Options, a następnie za pomocą przycisku Build utworzymy nasz przykładowy projekt. Spójrzmy teraz na pliki utworzone przez Glade w katalogu projektu.
Powinien się tam znaleźć plik .glade — to właśnie w nim Glade przechowuje projekt, używając języka XML.
Nieco więcej na temat XML powiemy w rozdziale 23, który jest poświęcony obsłudze XML w systemie Linux (a w szczególności bibliotece gnome-xml, którą Glade wykorzystuje przy analizie struktur tego języka).
W naszym przykładowym programie o nazwie example.glade znajduje się część definiująca widżety ramki i przycisku. Analizując ten plik, można bardzo szybko zrozumieć, w jaki sposób Glade zapisuje projekt w języku XML.
...
<class>GtkFrame</class>
<child_name>GnomeDock:contents</child_name>
<name>frame1</name>
<border_width>10</border_width>
<label>This is the frame label</label>
<label_xalign>0</label_xalign>
<shadow_type>GTK_SHADOW_ETCHED_IN</shadow_type>
<widget>
<class>GtkVBox</class>
<name>vbox1</name>
<homogeneous>False</homogeneous>
<spacing>7</spacing>
<widget>
<class>GtkButton</class>
<name>click_me_button</name>
<can_focus>True</can_focus>
<signal>
<name>clicked</name>
<handler>on_click_me_button_clicked</handler>
</signal>
...
W katalogu src umieszczone są pliki źródłowe projektu, które domyślnie mają następujące nazwy:
Nazwa pliku |
Opis |
Czy są modyfikowalne dla użytkownika? |
src/interface.h src/interface.h |
Zawiera funkcje, które można wywołać w celu utworzenia okien i okienek dialogowych dialogów zbudowanych przez Glade. |
NIE |
src/support.h src/support.c |
Funkcje pomocnicze dla Glade, włącznie z lookup_widget, którego można używać w celu uzyskania wskaźników do widżetów. |
NIE |
src/main.c |
Zawiera main. Początkowo tworzy i wyświetla jedno okno i okienko dialogowe w celach pokazowych. Nie jest modyfikowany przez Glade przy kolejnych przebudowach. |
TAK |
src/callbacks.h src/callbacks.c |
Wstępnie zawiera puste funkcje wywołania zwrotnego, które były skojarzone z sygnałami na zakładce Signals w okienku dialogowym Properties. |
TAK |
Pliki interface i support zawierają kod utworzony przez Glade, który jest zamieniany na nowy podczas kolejnej przebudowy, a więc nigdy nie należy dopisywać tam własnego kodu. Gwarantuje to, że plik interface.c zawsze może być odtworzony z pliku .glade. Podczas dodawania kolejnych wywołań zwrotnych do już istniejącego projektu Glade dopisuje (a nie zamienia) odpowiednie deklaracje i puste funkcje wywołań zwrotnych odpowiednio do plików callbacks.h i callbacks.c, dzięki czemu możliwe jest zespołowe tworzenie kodu i interfejsu użytkownika.
Jeżeli usuniemy plik tworzony przez Glade (np. plik callbacks.c), to zostanie on utworzony na nowo.
W pliku interface.c są zawarte funkcje tworzące każde okno i okienko dialogowe zaprojektowane za pomocą Glade. W naszym przykładzie mamy widżet GnomeApp o nazwie App1, a więc w pliku interface.c znajduje się tworząca go funkcja o nazwie create_app1. Oto początkowy fragment tego pliku:
GtkWidget*
create_app1 (void)
{
GtkWidget *app1;
GtkWidget *dock1;
GtkWidget *toolbar1;
GtkWidget *tmp_toolbar_icon;
GtkWidget *button1;
GtkWidget *button2;
GtkWidget *button3;
...
Każde okno w projekcie w pliku interface.c ma przyporządkowaną funkcję create_<nazwa_okna>. Funkcja ta zwraca jedynie wskaźnik do nowego okna, więc aby faktycznie przesłać okno na ekran, należy użyć wywołania gtk_widget_show.
lookup_widget
Jedno z pytań najczęściej zadawanych przez osoby nieobeznane z Glade brzmi: w jaki sposób przechwycić wskaźniki do widżetów wbudowanych w okno utworzone przez Glade, aby można było nimi manipulować? Dlaczego tak jest? Jak widzieliśmy przed chwilą, plik interface.c zawiera funkcje tworzące każdego okna zaprojektowanego za pomocą Glade. Funkcje te zwracają wskaźnik do widżetu tego okna, dlatego nie istnieje prosty mechanizm przejmowania wskaźników do widżetów wbudowanych w widżety najwyższego poziomu. Na przykład, wskaźnik GtkWidget w naszym przykładzie jest lokalny i prywatny dla create_app1.
Rozwiązaniem tego problemu jest funkcja lookup_widget zawarta w pliku support.c, który jest automatycznie tworzony przez Glade. Załóżmy, że mamy wskaźnik do widżetu o nazwie text_entry, zawierający okienko dialogowe message_box. Wskaźnik ten możemy uzyskać, wywołując funkcję lookup_widget z nazwą widżetu, do którego szukamy wskaźnika, podobnie jak w przypadku wskaźnika do message_box.
Prototyp lookup_widget ma następującą postać:
GtkWidget* lookup_widget (GtkWidget *widget, const gchar *widget_name)
a nasz przykład może wyglądać następująco:
GtkWidget *entry;
entry = lookup_widget(GTK_WIDGET(message_box), "text_entry");
Nazwa widżetu — w tym wypadku jest to „text_entry” — jest nazwą wpisaną w oknie dialogowym Properties, zaś widget nie musi być wskaźnikiem do tego widżetu nadrzędnego: może on być wskaźnikiem do dowolnego widżetu zawartego w tym samym oknie albo, co jest oczywiste, wskazywać na to samo okno.
Jest to bardzo ważna właściwość, ponieważ często nie mamy łatwego dostępu do wskaźnika do widżetu nadrzędnego (przynajmniej wówczas, gdy nie chcemy używać zmiennych globalnych), zaś wskaźnik do widżetu żądanego jest łatwo osiągalny. Na przykład w typowej funkcji wywołania zwrotnego mamy wskaźnik do widżetu wytwarzającego sygnał, ale jeżeli chcemy zmodyfikować lub sprawdzić inny widżet, to możemy po prostu użyć funkcji lookup_widget.
Dodajmy teraz trochę kodu do naszej przykładowej aplikacji Glade i użyjmy funkcji lookup_widget.
Dopisywanie kodu
Plik src/main.c w naszym przykładzie powinien wyglądać podobnie jak poniższy — zależy to od opcji ustawionych w Glade:
/*
* Początkowy plik main.c utworzony przez Glade. Można go dowolnie
* modyfikować. Glade nie podmienia tego pliku.
*/
#ifdef HAVE_CONFIG_H
# include <config.h>
#endif
#include <gnome.h>
#include "interface.h"
#include "support.h"
int
main (int argc, char *argv[])
{
GtkWidget *app1;
bindtextdomain (PACKAGE, PACKAGE_LOCALE_DIR);
textdomain (PACKAGE);
gnome_init ("example", VERSION, argc, argv);
/*
* Podany niżej kod został dodany przez Glade i służy do utworzenia
* wszyskich pojedynczych elementów (z wyjątkiem rozwijanych menu), tak
* aby można było coś zobaczyć po zbudowaniu projektu. Można usunąć
* dowolny element, który początkowo ma nie być widoczny.
*/
app1 = create_app1 ();
gtk_widget_show (app1);
gtk_main ();
return 0;
}
Okno app1 w naszej aplikacji GnomeApp jest tworzone za pomocą funkcji create_app1 i można je wyświetlić za pomocą funkcji gtk_widget_show. W tym prostym przypadku funkcja main utworzona przez Glade jest dokładnie taka, jakiej potrzebujemy — zostawimy ją więc bez modyfikacji.
W pliku callbacks.c znajduje się kilkanaście pustych funkcji wywołania zwrotnego. Jak pamiętamy, widżet GnomeApp ma wstępnie zdefiniowanych kilka pasków narzędziowych i pozycji menu, których nie będziemy usuwali. Oczywiście, są tu także funkcje wywołania zwrotnego zdefiniowane przez nas samych, czyli on_quit_button_clicked i on_click_me_button_clicked.
Do tych funkcji dopiszemy podany niżej kod, pamiętając o tym, że ponieważ nazwa przycisku została zmieniona na click_me_button, to odpowiednia funkcja wywołania zwrotnego będzie się nazywać on_click_me_button_clicked:
void
on_quit_button_clicked (GtkButton *button,
gpointer user_data)
{
gtk_main_quit();
}
void
on_click_me_button_clicked (GtkButton *button,
gpointer user_data)
{
GtkWidget *entry, *dialog;
const gchar *text;
entry = lookup_widget(GTK_WIDGET(button), "text_entry");
text = gtk_editabale_get_chars(GTK_EDITABLE(entry), 0, -1);
dialog = gnome_ok_dialog(text);
gtk_widget_show(dialog);
}
Wywołanie gtk_main_quit w funkcji on_quit_button_clicked powoduje to, co sugeruje nazwa, czyli zamknięcie aplikacji. Często chcemy, aby użytkownik potwierdził swój zamiar zamknięcia programu w oknie dialogowym, ale ten problem omówimy później.
W funkcji on_click_me_button_clicked przechwytujemy wskaźnik do widżetu obsługującego edycyjne pole tekstowe, korzystając z funkcji lookup_widget, a następnie pobieramy zawartość text_entry i przekazujemy ją do gnome_ok_dialog. Do pobrania zawartości text_entry użyliśmy tu gtk_editable_get_chars, ponieważ rodzima metoda gtk_entry_get_text zwraca wskaźnik na przydzielany wewnętrznie do widżetu obszar pamięci, który nie może być zwalniany, modyfikowany lub przechowywany. GtkEntry pochodzi bezpośrednio od GtkEditable.
Aby po raz pierwszy skompilować naszą przykładową aplikację, należy najpierw uruchomić skrypt autogen.sh:
./autogen.sh
make
./src/example
Powinniśmy wówczas zobaczyć coś takiego:
|
Po naciśnięciu przycisku otrzymamy komunikat:
|
Jak widać, naszą prostą aplikację Gnome utworzyliśmy bardzo szybko i bez kłopotów przez skorzystanie z Glade. W rzeczywistości omówione tu etapy wystarczają do tego, aby zrozumieć, w jaki sposób ma być skonstruowana bardziej skomplikowana aplikacja obsługująca wypożyczalnię płyt DVD. Zanim rozpoczniemy budowę interfejsu dvdstore zapoznamy się jeszcze z libglade, czyli z alternatywną metodą korzystania z projektów Glade.
libglade
W omówionej wyżej przykładowej aplikacji pakiet Glade został wykorzystany do budowy kodu ze źródeł zapisanych w XML. Teoretycznie jest to etap nadmiarowy, ponieważ dane XML w pełni opisują projekt interfejsu, a dla danego pliku .glade zawsze na wyjściu tworzony jest odpowiadający mu plik interface.c. W tym procesie tworzenia deklaracje XML są przekształcane na kod specyficzny dla danego języka programowania i danych bibliotek, tracąc swój ogólny charakter.
Co się stanie, jeśli rozbiór kodu XML utworzonego przez Glade nie będzie dokonywany w czasie kompilacji, lecz zamiast tego kod ten będzie interpretowany po uruchomieniu programu? Takie właśnie działanie osiągamy za pomocą libglade.
Aplikacje korzystające z libglade mogą podczas pracy dynamicznie pobierać projekt interfejsu w postaci pliku XML utworzonego przez Glade. Umożliwia to zmianę projektu bez potrzeby jego powtórnej kompilacji, a nawet wybór interfejsu przez samą aplikację spośród kilku dostępnych wariantów.
Użycie libglade w praktyce jest całkiem łatwe. Po prostu ładuje się plik .glade i tworzy według życzenia każde okno, korzystając z API. Podłączanie sygnałów jest także łatwe, ponieważ biblioteka libglade sama lokalizuje funkcje wywołania zwrotnego odpowiadające wywołaniom zwrotnym zdefiniowanym w Glade.
Aby sprawdzić, jak to działa w praktyce, w naszym przykładowym programie Glade zastosujemy libglade do interpretacji pliku .glade. Ponieważ teraz Glade nie będzie tworzyć całego drzewa kodu źródłowego, sami musimy utworzyć własny katalog i plik makefile.
Utworzyć nowy katalog o nazwie libglade_example i skopiować do niego z katalogu z przykładowym projektem pliki example.glade (lub inny projekt o wybranej przez nas nazwie), callbacks.c i callbacks.h.
Mamy zamiar nieco podstępnie wykorzystać poprzednio utworzony plik callbacks.c, oszczędzając w ten sposób sporo wysiłku. Przemianujemy więc callbacks.c na libglade_example.c i dopiszemy do niego funkcję main.
Zmienić nazwę callbacks.h na libglade_example.h.
Zmodyfikować callbacks.c, usuwając z dyrektyw include wywołania interface.h i support.h oraz używając zamiast callbacks.h tak, jak opisano wyżej. Dodać globalną deklarację zmiennej GladeXML i zachować plik jako libglade_example.c.
#include <glade/glade.h>
#include <gnome.h>
#include "libglade_example.h"
GladeXML *xml;
...
Obiekt xml jest kopią opisu interfejsu w języku XML. Deklarujemy tu xml jako zmienną globalną, aby zachować do niej dostęp z funkcji wywołania zwrotnego. Można także przekazywać xml do funkcji wywołania zwrotnego jako argument w danych użytkownika, ale użyta tutaj metoda jest prostsza i zastosowaliśmy ją w celu zachowania zgodności z poprzednim przykładem.
Teraz należy dodać main do libglade_example.c:
gint main(gint argc, gchar *argv[])
{
GtkWidget *app1;
gnome_init("libglade_example", "0.1", argc, argv);
glade_gnome_init();
xml = glade_xml_new("example.glade", NULL);
if (!xml)
{
g_warning("Could not load interface");
return 1;
}
app1 = glade_xml_get_widget(xml, "app1");
gtk_widget_show(app1);
glade_xml_signal_autoconnect(xml);
gtk_main();
return 0;
}
Pierwszą rzeczą rzucającą się w oczy w funkcji main jest wywołanie glade_gnome_init, inicjujące działanie Glade i procedur używanych przy tworzeniu widżetów. Następnie ładowany jest plik example.glade za pomocą wywołania glade_xml_new, którego prototyp jest następujący:
GladeXML* glade_xml_new (const char *fname, const char *root);
Drugi argument umożliwia budowę tylko fragmentu interfejsu, poczynając od widżetu root. Jest to przydatne np. wówczas, gdy chcemy utworzyć tylko pasek narzędziowy.
Funkcja glade_xml_get_widget jest równoważnikiem funkcji lookup_widget używanej w Glade. Zwraca ona wskaźnik widżetu o nazwie podanej jako drugi argument. Użyjemy jej do pobrania wskaźnika widżetu app1, który będziemy wyświetlać na ekranie.
Sygnały podłączamy do ich funkcji obsługujących (ang. handlers), korzystając z wywołania glade_xml_signal_autoconnect dopasowującego nazwy tych funkcji (podane w opisie interfejsu) do odpowiednich funkcji wywołania zwrotnego. Aby to wszystko działało, funkcje wywołania zwrotnego muszą występować w tablicy symboli w aplikacji i dlatego nie można ich deklarować jako statycznych.
W naszym przykładzie podłączone zostaną dwa sygnały pochodzące od kliknięcia przycisków. Musimy tutaj dokonać drobnej modyfikacji w funkcji obsługującej click_me_button, by w celu pobrania wskaźnika widżetu GtkEntry używała ona glade_xml_get_widget, a nie lookup_widget.
Na zakończenie musimy utworzyć plik Makefile.
Zmodyfikować funkcję on_click_me_button_clicked w pliku libglade_examaple.c do następującej postaci:
void
on_click_me_button_clicked (GtkButton *button,
gpointer user_data)
{
GtkWidget *entry, *dialog;
const gchar *text;
entry = glade_xml_get_widget(xml, "text_entry");
text = gtk_editable_get_chars(GTK_EDITABLE(entry), 0, -1);
dialog = gnome_ok_dialog(text);
gtk_widget_show(dialog);
}
Teraz należy utworzyć plik Makefile o następującej zawartości:
CC = gcc
CFLAGS = -g -Wall `gnome-config --cflags gnomeui libglade`
LDFLAGS = `gnome-config --libs gnomeui libglade`
all: libglade_example
clean:
rm -f*.o libglade_example
Aby uruchomić przykład korzystający z libglade, używamy poleceń:
$ make
$ ./libglade_example
Przykład aplikacji wykorzystującej libglade jest funkcjonalnie taki sam, jak przykład poprzedni. Można więc załadować example.glade ponownie do Glade, zmodyfikować układ widżetów, zachować projekt i uruchomić go bez potrzeby powtórnej kompilacji.
GUI do obsługi wypożyczalni płyt DVD
W tym podrozdziale połączymy wszystkie omawiane dotychczas zagadnienia w jedną działającą całość i utworzymy za pomocą Glade kod interfejsu dla aplikacji obsługującej wypożyczalnię DVD. W projekcie wykorzystamy język C (a nie libglade) aby uzyskać podobieństwo do większości istniejących aplikacji GNOME.
Tak jak planowano, interfejs GNOME nie będzie wymagał znajomości kodu samej aplikacji obsługującej bazę danych, który to kod jest ukryty za interfejsem programowym (API). Nasz program będzie wymieniał dane z tym kodem, korzystając z okien dialogowych poprzez funkcje API. Tworzenie interfejsu GNOME jest w rzeczywistości bardzo proste i jedynym problemem jest tu brak miejsca na pełny opis wszystkich części API.
Istnieje kilka aplikacji GNOME, które są uniwersalnymi klientami baz danych przeznaczonymi do bezpośredniej współpracy z bazami. Jednym z najbardziej wydajnych narzędzi jest gnome-db, zapewniające obsługę baz PostgreSQL, MySQL, ODBC, Oracle i innych, z programem klienta zapisanym jako widżet. Glade umożliwia współpracę z gnome-db, począwszy od wersji 0.5.9. Możemy sobie wyobrazić włączenie takiego widżetu jako części naszego interfejsu GNOME do obsługi wypożyczalni DVD, co umożliwi bezpośredni dostęp do wydajnych zapytań kierowanych do bazy. Jest to także dobrą wymówką, aby nie tworzyć pewnych funkcji raportujących obsługę danych w API i skoncentrować się na podstawowych funkcjach API obsługujących wypożyczanie, zwroty i wyszukiwanie filmów DVD.
Do naszego interfejsu dodamy następujące właściwości:
Sprawdzanie tożsamości (ang. authentication) użytkownika bazy danych logującego się za pomocą okna dialogowego lub wiersza poleceń.
Zapis każdej transakcji wyświetlanej w oknie logu i zachowywanej w pliku.
Funkcje dopisywania, modyfikacji i usuwania klienta wypożyczalni.
Funkcje dopisywania, modyfikacji i usuwania tytułów filmów.
Możliwość wyszukiwania tytułów i klientów.
Możliwość określenia statusu wypożyczenia płyty.
Wypożyczanie tytułów klientom.
Zwrot poszczególnych płyt z wyświetleniem terminowości zwrotu.
Rezerwacja tytułów.
Zachowywanie konfiguracji.
Zarządzanie sesją.
Projekt
W dalszych częściach książki jako nazwę projektu interfejsu będziemy stosować dvdstore.
Nie chcemy tu poświęcać zbyt wiele czasu na rozważania sposobów tworzenia interfejsu graficznego za pomocą Glade. Wszystko to Czytelnik może sprawdzić sam, pobierając projekt i kod z serwera ftp wydawnictwa Helion (ftp://ftp.helion.pl/przyklady/zaprli.zip). Otwierając za pomocą Glade plik dvdstore.glade oraz korzystając z okien Properties i Widget Tree, można szybko poznać projekt każdego okna i sprawdzić sposób łączenia ze sobą sygnałów od poszczególnych widżetów.
Przy wyborze, grupowaniu i nadawaniu ikon elementom menu i paska narzędziowego oraz przydzielaniu skrótów klawiszowych kierowano się po prostu zdrowym rozsądkiem i wygodą. Aplikacja dvdstore nie jest typowa (z pozycjami New, Open, Close w menu File, itp.), lecz zmusza do wyboru najbardziej zbliżonych równoważnych elementów. Z praktyki wynika, że podczas konstrukcji interfejsu graficznego kopiuje się bez skrupułów podobną aplikację, dbając oczywiście o to, aby projekt nie wyglądał dziwnie lub nie powodował problemów w GNOME. Na przykład rozmiar paska narzędziowego GNOME ustawiany jest zgodnie z szerokością i wysokością największego przycisku, a więc tekst na nim powinien być jak najkrótszy — w wyniku otrzymamy wówczas bardziej zwarty pasek narzędziowy.
Kompilacja i uruchamianie dvdstore
Omawianie znaczącej części kodu kryjącego się w dvdstore może być bardzo kłopotliwe, głównie ze względu na jego rozmiar, a nie złożoność. Najpierw omówimy zatem sposób korzystania z dvdstore, następnie omówimy ogólnie strukturę kodu opisującego okna dialogowe, a na koniec rozważymy szczegółowo ważniejsze fragmenty kodu.
Rozpoczniemy od pobrania dvdstore z serwera ftp wydawnictwa Helion (ftp://ftp.helion.pl/przyklady/zaprli.zip), rozpakowania archiwum ze źródłami, kompilacji, instalacji i uruchomienia w tradycyjny sposób:
$ ./configure
$ make
$ su -
Password:
# make install
# dvdstore
Plik src/Makefile.am jest domyślnie skonfigurowany do współpracy z bazą PostgreSQL, lecz modyfikując jego zawartość można przystosować dvdstore do obsługi prostego pliku tekstowego.
Polecenie make install jest opcjonalne i normalnie musi być uruchamiane przez uprzywilejowanego użytkownika. Jeżeli decydujemy się na jego użycie, to plik dvdstore.desktop zostanie skopiowany do katalogu $(gnomedir)/gnome/Applications i wówczas w menu panelu GNOME w sekcji aplikacji pojawi się wpis DVD Store.
Po uruchomieniu dvdstore aplikacja rozpoczyna działanie, nie będąc połączona z bazą danych, z nieaktywnymi menu i ikonami na pasku narzędziowym oraz napisem Not Connected widocznym na pasku statusu.
|
Po kliknięciu przycisku Connect (lub użyciu klawiszy Ctrl-C) otwiera się okienko dialogowe, w którym należy wprowadzić nazwę użytkownika bazy danych PostgreSQL i kliknąć OK. Jeśli wszystko pójdzie dobrze, dvdstore połączy się z bazą, a ikony i menu staną się aktywne. Pojawi się też wpis w oknie logu i napis Connected na pasku statusu.
|
|
Spróbujmy teraz dopisać nowy tytuł filmu. Po wyborze DVDstore | New Title otwiera się okno dialogowe Title. Wpisujemy w nim dane filmu i klikamy na przycisk Add. Pojawi się wówczas okno z informacją o identyfikatorze dodanego filmu oraz nowy wpis w logu.
|
Następnie dodajmy nową płytę do danego tytułu, wybierając z menu DVDStore | New Disk i wprowadzając identyfikator tytułu w pole Title ID. Pojawi się wówczas okno informacyjne zawierające wartość wprowadzoną po kliknięciu przycisku Add.
|
Dodajmy teraz nowego klienta wypożyczalni, wywołując następne okienko dialogowe z menu DVDStore | New Member lub klikając odpowiedni przycisk na pasku narzędziowym.
|
Można także sprawdzić, jak działa wyszukiwanie, korzystając z menu Edit | Find lub przycisku Search. Okno wyszukiwania ma trzy zakładki: Titles, Members i Disks, na których można podawać kryteria wyszukiwania tytułu, klienta i płyty. Na pasku statusu w tym oknie jest wyświetlana liczba wyszukanych wpisów.
|
Na zakładkach Title i Member kliknięcie wpisu prawym klawiszem myszy powoduje wyświetlenie menu pomocniczego, które zawiera pozycje Rent, Reserve, Edit i Delete — umożliwiając bezpośredni dostęp do tych funkcji. Na zakładce Member można wypożyczyć (Rent) lub zarezerwować (Reserve) dany wpis dla wybranego klienta.
Spróbujmy teraz wykonać operację wypożyczenia jakiegoś filmu. Wprowadźmy w tym celu poprawny identyfikator klienta (Member ID) i wyświetlmy listę identyfikatorów tytułów (Title ID) w oknie dialogowym Edit | Rent. Po wyborze tytułu należy kliknąć przycisk Rent; wtedy pojawi się okno informujące o stanie wypożyczenia. W oknie tym jest wyświetlona lista tytułów i identyfikatorów płyt oraz odpowiednie znaczki informujące o tym, czy dana płyta jest wypożyczona, czy nie.
|
|
Na powyższym rysunku pokazano wygląd tego okna po próbie wypożyczenia dwóch kopii danego filmu w sytuacji, gdy wpisana była tylko jedna płyta (a więc tylko jedno żądanie wypożyczenia mogło się zakończyć pomyślnie).
Spróbujmy następnie zwrócić płytę. Wyświetlamy w tym celu okno dialogowe dla zwrotów, klikając przycisk Return i wprowadzając listę identyfikatorów zwracanych płyt. Na tę listę mogą być wpisane identyfikatory tylko tych płyt, które zostały wypożyczone.
Kolumna statusu zawiera informację o przekroczeniu terminu zwrotu. W jaki sposób aplikacja dvdstore pobiera taką informację? Po otwarciu okna konfiguracyjnego (menu Settings | Preferences) widzimy pole Number of Days Renting before overdue (czyli okres wypożyczenia). Program dodaje liczbę dni z tego pola do daty wypożyczenia i porównuje otrzymaną wartość z bieżącą datą. Status płyty zmienia się na OVERDUE (przeterminowany), jeśli bieżąca data jest późniejsza niż otrzymany wynik. W pozostałych przypadkach status płyty ma wartość OK.
|
W oknie Preferences jest także wyświetlana nazwa pliku z logiem (ang. logfile), w którym są przechowywane kopie informacji o wykonywanych operacjach. Na drugiej zakładce umieszczono kilka przycisków dwustanowych, za pomocą których można ustalić, czy w oknie wyszukiwania będą wyświetlane informacje z pól Title, czy Member.
|
Struktura
Zapoznawszy się z działaniem aplikacji dvdstore, możemy bliżej zapoznać się z jej szczegółami. Najlepiej rozpocząć od załadowania definicji interfejsu dvdstore.glade do Glade i dokładnie ją zbadać. W poniższej tabeli znajduje się krótki opis każdego okna i menu zaprojektowanego za pomocą Glade, czyli elementów tworzących naszą aplikację dvdstore.
Nazwa widżetu |
Rodzaj widżetu |
Plik z kodem funkcji wywołania zwrotnego |
Opis |
dvdstore |
GnomeApp |
callbacks.c |
Okno główne |
about_dialog |
GnomeAbout |
-- |
Okno-wizytówka autora |
member_dialog |
GnomeDialog |
member_dialog.c |
Dopisywanie lub modyfikacja klienta |
rent_dvd_dialog |
GnomeDialog |
rent_dvd_dialog.c |
Wypożyczenie filmu |
return_dvd_dialog |
GnomeDialog |
return_dvd_dialog.c |
Zwrot płyty |
reserve_dialog |
GnomeDialog |
reserve_dialog.c |
Zwrot tytułu |
dvd_dialog |
GnomeDialog |
title_dialog.c |
Dopisywanie lub edycja tytułu |
member_optionmenu |
GtkMenu |
-- |
Opcje wyszukiwania klienta |
search_window |
GtkWindow |
search_window.c |
Okno wyszukiwania |
dvd_popup_menu |
GtkMenu |
search_window.c |
Menu pomocnicze z rozwijaną listą wyszukiwania |
disk_dialog |
GnomeDialog |
disk_dialog.c |
Dopisywanie płyty |
rent_report_dialog |
GnomeDialog |
-- |
Wynik wypożyczenia |
preferences |
GnomePropertyBox |
properties.c |
Okno właściwości |
login_dialog |
GnomeDialog |
-- |
Okno logowania |
Można się zastanawiać, czemu służy pozycja member_optionmenu. Na zakładce Member w oknie wyszukiwania znajduje się widżet GtkOptionmenu, który za pomocą rozwijanego menu umożliwia wybór sposobu wyszukiwania klienta albo na podstawie jego identyfikatora, albo nazwiska. Widżet ten pobiera jako swoje opcje GtkMenu i dlatego zdecydowaliśmy się zdefiniować takie menu za pomocą Glade, nazwawszy je member_optionmenu.
Jak się już przekonaliśmy, cały projekt jest skoncentrowany wokół dwóch okien. Okno główne GnomeApp zawiera menu, pasek narzędziowy oraz obszar z logiem, zaś okno wyszukiwania umożliwia znalezienie informacji o tytułach, klientach i płytach. Istnieje także kilka okienek dialogowych obsługujących wprowadzanie danych przez użytkownika oraz okno dialogowe rent_report_dialog, które wyświetla tylko informacje o płycie wypożyczonej danemu klientowi.
Każde z okienek dialogowych ma swój kod przechowywany w odpowiednim pliku .c o nazwie podanej w powyższej tabeli. Pozostała część kodu źródłowego zawiera funkcję main i kilka różnych funkcji pomocniczych.
Plik |
Opis |
main.c |
Zawiera funkcje do analizy opcji wiersza poleceń, inicjalizacji i tworzenia aplikacji dvdstore. |
dvd_gui.h |
Zawiera zmienne globalne programu. |
misc.c |
Zawiera funkcje służące do łączenia i rozłączania się z bazą danych, śledzenia stanu aktywności widżetów, obsługi zakończenia działania aplikacji, obsługi zapisów w logu, obsługi błędów, obliczania terminowości zwrotu płyty i wyświetlania winietki About. |
session.c |
Zawiera funkcje do zarządzania sesją. |
Kod
Rozpocznijmy nasz opis kodu od obowiązkowego miejsca, czyli od main.c.
main.c
Za wymaganymi plikami nagłówkowymi tworzymy strukturę poptOption:
struct poptOption options[] =
{
{
"username", 'u', POPT_ARG_STRING, &user, 0,
N_("Specify a user"), N_("USER")
},
{
"password", 'p', POPT_ARG_STRING, &passwd, 0,
N_("Specify a password"), N_("PASSWORD")
},
{
NULL, '\0', 0, NULL, 0,
NULL, NULL
}
};
Zwróćmy uwagę na użycie makrodefinicji N_ odnoszących się do tekstu, który ma się pojawić na ekranie. Są one bardzo użyteczne, gdy decydujemy się użyć różnych wersji językowych, ponieważ ułatwiają podstawianie tekstów w innych językach w wybranych polach.
W funkcji main ustawiamy język narodowy (patrz rozdział 28.) i wywołujemy gnome_init_with_popt_table w celu inicjacji GNOME i przetworzenia opcji wiersza poleceń:
int
main (int argc, char *argv[])
{
GnomeClient *client;
#ifdef ENABLE_NLS
bindtextdomain (PACKAGE, PACKAGE_LOCALE_DIR);
textdomain (PACKAGE);
#endif
gnome_init_with_popt_table("dvdstore", VERSION, argc, argv,
options, 0, NULL);
Następnie ustawiamy funkcję wywołania zwrotnego dla zarządzania sesją:
/* Session Management */
client = gnome_master_client();
gtk_signal_connect (GTK_OBJECT(client),
"save_yourself",
GTK_SIGNAL_FUNC(save_session),
argv[0]);
gtk_signal_connect (GTK_OBJECT(client),
"die"
GTK_SIGNAL_FUNC (session_die),
NULL);
Teraz tworzymy okno główne, podłączamy jego sygnał delete_event do exit_dvdstore (z pliku misc.c) i otwieramy plik logu:
main_window = create_dvdstore ();
gtk_signal_connect(GTK_OBJECT(main_window),
"delete_event",
GTK_SIGNAL_FUNC(exit_dvdstore),
NULL);
open_log_file();
Przed wyświetleniem main_window aktualizujemy pasek appbar, aby pokazywał aktualny status, a także zmieniamy stan menu i widżetów paska narzędziowego na nieaktywny ("Not Connected"). Następnie dodajemy do logu komunikat powitalny i jeżeli w wierszu poleceń była użyta nazwa użytkownika — próbujemy połączyć się z bazą danych:
gnome_appbar_push(GNOME_APPBAR(lookup_widget(main_window, "appbar1")),
"Not Connected");
sensitize_widgets (main_window, FALSE);
gtk_widget_show (main_window);
add_log_message(_("*** Welcome to the DVDstore ***));
if (user)
dvd_store_connect();
gtk_main ();
return 0;
}
callbacks.c
Jak już widzieliśmy, plik callback.c zawiera funkcje wywołań zwrotnych dla elementów menu i paska narzędziowego głównego okna GnomeApp. Są one zgrupowane w tym jednym pliku wyłacznie dla wygody. Wszystkie funkcje wywołania zwrotnego są po prostu przekierowaniami do właściwych funkcji umieszczonych w innych plikach źródłowych.
Na przykład funkcja wywołania zwrotnego dla przycisku Rent Title z paska narzędziowego w celu wyświetlenia okna dialogowego obsługującego wypożyczenie filmu wywołuje do_rent_dvd_dialog z pliku rent_dialog.c:
void
on_rent_button_clicked (GtkButton *button,
gpointer user_data)
{
do_rent_dvd_dialog(NULL, 0);
}
Zamiast wypisywać pełną zawartość pliku callbacks.c, podamy tylko jej krótkie podsumowanie w poniższych tabelach:
|
Funkcja wywołania zwrotnego |
Funkcja, która jest wywoływana przez funkcję wywołania zwrotnego |
Plik źródłowy funkcji |
Pozycja menu |
on_connect_activate |
dvd_store_connect |
misc.c |
|
on_disconnect_activate |
dvd_store_disconnect |
misc.c |
|
on_add_member_activate |
do_member_dialog |
member_dialog.c |
|
on_add_dvd_activate |
do_dvd_dialog |
title_dialog.c |
|
on_new_disk_activate |
do_new_disk_dialog |
disk_dialog.c |
|
on_exit_activate |
exit_dvdstore |
misc.c |
|
on_search_activate |
do_search_dialog |
search_window.c |
|
do_return_dvd_activate |
do_return_dvd_dialog |
return_dialog.c |
|
on_rent_dvd_activate |
do_rent_dvd_dialog |
rent_dialog.c |
|
on_reserve_activate |
do_reserve_dialog |
reserve_dialog.c |
|
on_preferences_activate |
do_property_box |
properties.c |
|
on_about_activate |
do_about_dialog |
misc.c |
|
Funkcja wywołania zwrotnego |
Funkcja, która jest wywoływana przez funkcję wywołania zwrotnego |
Plik źródłowy funkcji |
Przyciski paska narzędziowego |
on_connect_button_clicked |
dvd_store_connect |
misc.c |
|
on_disconnect_button_clicked |
dvd_store_disconnect |
misc.c |
|
on_rent_button_clicked |
do_rent_dvd_dialog |
rent_dialog.c |
|
on_return_button_clicked |
do_return_dvd_dialog |
return_dialog.c |
|
on_add_member_button_clicked |
do_member_dialog |
member_dialog.c |
|
on_search_button_clicked |
do_search_dialog |
search_window.c |
|
on_reserve_button_clicked |
do_reserve_dialog |
reserve_dialog.c |
|
on_exit_button_clicked |
exit_dvdstore |
misc.c |
Można rodzić się pytanie, po co stosujemy takie funkcje pośrednie? Czyż nie lepiej podłączyć sygnały bezpośrednio do właściwych funkcji? Nie możemy wyjaśnić, dlaczego nie moglibyśmy tak postępować, ale mamy proste wyjaśnienie, dlaczego tak postąpiliśmy.
Jak już wiemy, Glade dodaje puste funkcje wywołań zwrotnych dla każdego nowego wywołania, łącznie z jego prototypem, odpowiednio do plików callbacks.c i callbacks.h. Moglibyśmy przenieść te funkcje do dowolnego kodu źródłowego, ale w celu ułatwienia ich lokalizacji przy przeglądaniu kodu wygodniej jest trzymać je w jednym miejscu. Jest to szczególnie ważne wówczas, gdy wiadomo, że kod aplikacji był tworzony za pomocą Glade, bowiem znane są wtedy pochodzenie i zadania pliku callbacks.c.
member_dialog.c oraz title_dialog.c
Pliki member_dialog.c oraz title_dialog.c zawierają kod źródłowy obsługujący dodawanie i modyfikację klientów wypożyczalni oraz tytułów filmów DVD. Są one podobne do siebie i dlatego omawianie szczegółów rozpoczynamy właśnie od nich.
Plik member_dialog.c zawiera tylko dwie funkcje: jedna tworzy okno dialogowe, wypełniając odpowiednio jego zawartość, a druga jest wywołaniem zwrotnym podłączonym do sygnału „clicked” tego okna. Zarówno dla tworzenia, jak i modyfikacji klientów użyty jest ten sam szablon okna dialogowego Glade, ponieważ obydwie operacje wymagają tych samych pól. Użyta jest tutaj także jedna funkcja o nazwie do_member_dialog, obsługująca tworzenie i modyfikację klientów wypożyczalni.
Spodziewamy się tu wielu modyfikacji: zarówno tekstu w widżecie GtkEntry (czyli w widżecie wyświetlającym np. nazwisko klienta) odpowiadającego wartościom związanych z danym elementem opisującym klienta (czyli np. member.name), jak i na odwrót. Dlatego do obsługi tych zadań wprowadzamy dwie makrodefinicje, dzięki którym można będzie zaoszczędzić wiele pracy przy pisaniu kodu.
ENTRY_SET_TEXT(field) ustawia tekst w widżecie field zgodnie z wartością elementu member.field:
#define ENTRY_SET_TEXT(field)
gtk_entry_set_text(
GTK_ENTRY(lookup_widget(member_dialog, #field)),
member.field
)
ENTRY_GET_TEXT(field) ustawia wartość elementu member.field zgodnie z tekstem wpisanym do widżetu field:
#define ENTRY_GET_TEXT(field, field_len)
strncpy(
(member.field),
gtk_entry_get_text(
GTK_ENTRY(lookup_widget(GTK_WIDGET(gnomedialog), #field))
),
field_len
)
[[[ramka]]]
Powyższe makrodefinicje działają tylko dlatego, że celowo użyto takich samych nazw dla widżetów GtkEntry w oknie dialogowym Glade i dla elementów struktury opisującej klienta wypożyczalni DVD.
[[[koniec ramki]]]
Funkcja do_dvd_dialog z pliku title_dialog.c ma dodatkową właściwość nadawania wartości rozwijanym listom Genre i Classification, które są pobierane poprzez API.
do_member_dialog
Funkcja do_member_dialog pobiera jeden argument będący albo żądanym identyfikatorem klienta, którego dane chcemy zmieniać, albo zerem, jeśli chcemy utworzyć nowego klienta. Następnie dokonywane jest sprawdzenie poprawności podanego identyfikatora i jeśli on istnieje — wstawienie do pól w oknie dialogowym bieżących wartości dla danego klienta. Aby użytkownik mógł łatwiej rozróżniać okno dialogowe modyfikacji danych klienta od okna tworzenia klienta, zmieniany jest jego tytuł i wypełniane pole przeznaczone na identyfikator (puste przy tworzeniu nowego klienta).
void
do_member_dialog(gint member_id_to_edit)
{
dvd_store_member member;
Najpierw deklarujemy wskaźnik member_dialog jako static, dzięki któremu na raz będzie mogła być otwarta tylko jedna kopia okna dialogowego do obsługi klienta.
static GtkWidget* member_dialog = NULL;
Wartość member_dialog jest równa NULL, gdy okno jest usuwane z połączenia z sygnałem „destroy”. Gdy ta wartość nie równa się NULL, musi ono istnieć w jakiejkolwiek postaci, np. zminimalizowane lub jako ikona. W takim wypadku staramy się je wywołać na pierwszy plan. Jest to jedyny moment, gdy korzystamy z „niskopoziomowych” metod gdk_window.
if (member_dialog != NULL)
{
/* Próba wywołania okna na pierwszy plan
*/
gdk_window_show(member_dialog->window);
gdk_window_raise(member_dialog->window);
}
else
{
/* Wywołanie funkcji utworzonej przez glade
* do utworzenia okna i podłączenia wywołań zwrotnych
*/
member_dialog = create_member_dialog ();
gtk_signal_connect(GTK_OBJECT(member_dialog),
"destroy",
GTK_SIGNAL_FUNC(gtk_widget_destroyed),
&member_dialog);
gnome_dialog_set_parent(GNOME_DIALOG(member_dialog),
GTK_WINDOW(main_window)));
gnome_dialog_set_close(GNOME_DIALOG(member_dialog),
TRUE);
Korzystamy tu z funkcji dvd_member_get w celu odszukania klienta, którego identyfikator jest argumentem tej funkcji i wypełniamy wszystkie pola odpowiednimi wartościami. Jeżeli wyszukiwanie się nie powiedzie (albo identyfikator jest równy zeru lub nie był podany), to wartości w polach pozostają bez zmian.
if (dvd_member_get(member_id_to_edit, &member) == DVD_SUCCESS)
{
/* poprawny numer klienta - wypełnienie pól bieżącymi wartościami */
gtk_label_set_text(GTK_LABEL(lookup_widget(member_dialog,
"member_no")), member.member_no);
ENTRY_SET_TEXT(title);
ENTRY_SET_TEXT(fname);
ENTRY_SET_TEXT(lname);
ENTRY_SET_TEXT(house_flat_ref);
ENTRY_SET_TEXT(address1);
ENTRY_SET_TEXT(address2);
ENTRY_SET_TEXT(town);
ENTRY_SET_TEXT(state);
ENTRY_SET_TEXT(zipcode);
ENTRY_SET_TEXT(phone);
gtk_window_set_title(GTK_WINDOW(member_dialog), _("Edit Member"));
}
gtk_widget_show (member_dialog);
}
}
on_member_dialog_clicked
Jest to funkcja wywołania zwrotnego obsługująca w oknie dialogowym sygnał „clicked”, emitowany przy kliknięciu dowolnego przycisku lub elementu sterującego w oknie. Parametr arg1 jest nieopisową nazwą zmiennej używanej przez Glade do przechowywania numeru przycisku związanego z sygnałem. Sprawdzamy, czy został naciśnięty przycisk OK i jeśli tak, to dane opisujące klienta wypożyczalni są modyfikowane (albo tworzone).
void
on_member_dialog_clicked (gnomeDialog *gnomedialog,
gint arg1,
gpointer user_data)
{
GtkWidget *message_box;
gchar *msg;
gchar *member_no;
gint member_id = 0;
Sprawdzamy, czy rzeczywiście został naciśnięty przycisk OK i jeśli tak było, pobieramy zawartość pól okna dialogowego:
if (arg1 == GNOME_OK)
{
dvd_store_member member;
gtk_label_get(
GTK_LABEL(lookup_widget(GTK_WIDGET(gnomedialog), "member_no")),
&member_no
);
strncpy((member.member_no), member_no, MEMBER)KNOWN_ID_LEN);
ENTRY_GET_TEXT(title, PERSON_TITLE_LEN);
ENTRY_GET_TEXT(fname, NAME_LEN);
ENTRY_GET_TEXT(lname, NAME_LEN);
ENTRY_GET_TEXT(house_flat_ref, NAME_LEN);
ENTRY_GET_TEXT(address1, ADDRESS_LEN);
ENTRY_GET_TEXT(address2, ADDRESS_len);
ENTRY_GET_TEXT(town, ADDRESS_len);
ENTRY_GET_TEXT(state, STATE_LEN);
ENTRY_GET_TEXT(zipcode, ZIP_CODE_LEN);
ENTRY_GET_TEXT(phone, PHONE_NO_LEN);
Jeżeli w oknie dialogowym znajdują się dane istniejącego klienta wypożyczalni, to etykieta member_no będzie zawierać jego numer. Jeżeli uda się pobrać identyfikator klienta na podstawie tej etykiety, wówczas będziemy wiedzieć, że modyfikowane są dane klienta istniejącego i można przekazać strukturę member do dvd_member_set.
if (
dvd_member_get_id_from_number(member_no, &member_id) == DVD_SUCCESS
)
{
member.member_id = member_id;
dvd_gui_show_result("member_set", dvd_member_set(&member));
}
Jeżeli wywołanie dvd_member_get_id_from_number nie powiedzie się, to wiemy, że należy utworzyć nowego klienta, pobrać jego nowy identyfikator i wyświetlić jego nowy numer w oknie informacyjnym.
else
{
dvd_gui_show_result("member_create",
dvd_member_create(&member, &member_id));
dvd_gui_show_result("member_get",
dvd_member_get(member_id, &member));
msg = g_strdup_printf(_("%s %s added as new member, no. %s"),
member.title, member.fname,
member.lname, member.member_no);
message_box = gnome_message_box_new (msg, GNOME_MESSAGE_BOX_INFO,
GNOME_STOCK_BUTTON_OK,
NULL);
gtk_widget_show(message_box);
add_log_message drukuje msg w oknie logowania i dołącza go do pliku logowania:
add_log_message(msg);
g_free(msg);
}
}
}
rent_dialog.c oraz return_dialog.c
Aplikacja obsługująca wypożyczalnię ma pozwalać na wypożyczanie i rezerwację tytułów i płyt w pakietach, by można było podać łączną cenę za każdą transakcję. Jak już wiemy, w dvdstore rozwiązano to tak, że do okna dialogowego dołączony jest widżet rozwijanej listy (ang. list widget). Zawiera on listę takich tytułów lub płyt, którą możemy dodać lub usunąć. Jest to szczególnie przydatne przy popełnieniu błędu. Następnie możemy wypożyczyć (lub zwrócić) całą listę w jednym etapie, klikając przycisk Rent lub Return.
|
Okna dialogowe wypożyczania i rezerwacji działają bardzo podobnie. Plik rent_dialog.c zawiera jednak dodatkowy kod obsługujący wyświetlanie okna z raportem informującym personel o tym, którą płytę należy wydać klientowi. Ze względu na swoje podobieństwo, omówimy tylko kod jednego z tych okien dialogowych, czyli rent_dialog.c.
Istnieje kilka elementów okna dialogowego obsługującego wypożyczenie, które wymagają dokładnego objaśnienia. Jako lista tytułów zastosowany jest widżet GtkClist, ponieważ jego API jest bardziej zwarte i elastyczne niż API prostszego widżetu GtkList.
Widżet GtkClist został tu użyty tylko jako element ekranowy, a nie miejsce do przechowywania danych. Innymi słowy, dane są do niego tylko wpisywane i nigdy nie jest odczytywana zawartość komórki. Ten niezwykły dla GTK+ widżet, jakim jest GtkClist, jest dość kłopotliwy w użyciu (podczas pisania tej książki), ma obszerny API i nie jest tak elastyczny, jakby mogło się wydawać. Oczekuje się, że w GTK+ 1.4 pojawi się nowy widżet obsługujący wyświetlanie drzew i list bardziej wydajnie i w zunifikowany sposób.
Dla celów pokazowych koncentrujemy się tutaj na specyficznej liście danych, a nie na widżecie obsługującym ich wyświetlanie i przechowywanie. Ma tutaj sens przechowywanie listy pozycji jako jednokierunkowej listy zdefiniowanej w glib i modyfikacja widżetu GtkClist za pomocą niezależnej funkcji update_rent_dvd_disk_clist w taki sposób, aby jego zawartość odzwierciedlała zawartość listy.
PIXMAP_HEIGHT definiuje pionowy rozmiar wiersza raportu wypożyczenia clist tak, aby obrazki znaczników nie były obcinane. LABEL_SET_TEXT jest kolejną przydatną makrodefinicją używaną do przechowywania powtórzeń.
#define PIXMAP_HEIGHT 19
#define LABEL_SET_TEXT(field)
gtk_label_set_text(
GTK_LABEL(lookup_widget(rent_report_dialog, #field),
g_strdup(member.field)
)
Element rent_disk_slist jest wspomnianą jednokierunkową listą zdefiniowaną w glib, w której znajduje się lista tytułów do wypożyczenia.
static GSList *rent_disk_slist;
do_rent_dvd_dialog
Funkcja ta służy do wyświetlenia okna dialogowego obsługującego wypożyczanie. Jako opcjonalny argument pobiera identyfikatory klienta i tytułu, wstawiane następnie do widżetów wejściowych przed ich wyświetleniem. Dzięki temu można wyszukiwać klienta lub tytuł w oknie wyszukiwania i wybierać jedną pozycję z listy. Po wyborze pozycji Rent w menu kontekstowym identyfikator klienta lub tytułu dla wybranej pozycji jest automatycznie wpisywany do okna dialogowego obsługującego wypożyczanie.
void
do_rent_dvd_dialog(gchar *default_member, gint default_title)
{
GtkSpinButton *title_id;
GtkWidget *member_no;
static GtkWidget *rent_dialog;
g_slist_free(rent_disk_slist);
rent_disk_slist = NULL;
if (rent_dialog != NULL)
{
/* Próba przeniesienia okna na pierwszy plan
*/
gdk_window_show(rent_dialog->window);
gdk_window_raise(rent_dialog->window);
}
else
{
rent_dialog = create_rent_dvd_dialog();
Sprawdzimy teraz, czy argumenty zawierają identyfikatory nie mające wartości NULL i jeśli tak jest, wypełnimy zawartość odpowiedniego widżetu:
title_id = GTK_SPIN_BUTTON(lookup_widget(rent_dialog,
"titleid_spinbutton"));
member_no = lookup_widget(rent_dialog, "member_no_entry");
if (default_title)
gtk_spin_button_set_value(title_id, (float) default_title);
if (default_member !+ NULL)
gtk_entry_set_text(GTK_ENTRY(member_no), default_member);
gtk_signal_connect(GTK_OBJECT(rent_dialog),
"destroy",
GTK_SIGNAL_FUNC(gtk_widget_destroyed),
&rent_dialog);
gnome_dialog_set_parent(GNOME_DIALOG(rent_dialog),
GTK_WINDOW(main_window));
gnome_dialog_set_close(GNOME_DIALOG(rent_dialog),
TRUE);
gtk_widget_show(rent_dialog);
}
}
on_rent_dvd_dialog_clicked
Jest to funkcja, gdzie obsługiwane są wszelkie działania wywoływane w odpowiedzi na kliknięcie dowolnego przycisku w oknie dialogowym. Po sprawdzeniu, że został kliknięty przycisk OK, pobierany jest numer klienta; w przeciwnym wypadku następuje wyjście z funkcji.
void
on_rent_dvd_dialog_clicked (GnomeDialog *gnomedialog,
gint arg1,
gpointer user_data)
{
GtkWidget *rent_report_dialog;
GtkCList *rent_result_clist;
GdkPixmap *tick;
GdkPixmap *cross;
GdkBitmap *tick_mask;
GdkBitmap *cross_mask;
dvd_title title;
dvd_store_member member;
gchar *msg;
gchar *text[4];
gchar *pathname;
gchar *member_no;
gint member_id, title_id, disk_id;
gint result, count;
if (arg1 == GNOME_OK)
{
member_no = gtk_entry_get_text(GTK_ENTRY(lookup_widget
(GTK_WIDGET(gnomedialog),
"member_no_entry")));
Tutaj ładujemy do pamięci mapy bitowe w formacie XPM zawierające obrazy znaczników „haczyka” (ang. tick) i „krzyżyka” (ang. cross), które będą używane w raporcie wypożyczenia GtkClist.
pathname = gnome_pixmap_file("yes.xpm");
tick = gdk_pixmap_colormap_create_from_xpm ( NULL,
gtk_widget_get_default_colormap(),
&tick_mask,
NULL,
pathname );
pathname = gnome_pixmap_file("no.xpm");
cross = gdk_pixmap_colormap_create_from_zpm ( NULL,
gtk_widget_get_default_colormap(),
&cross_mask,
NULL,
pathname);
g_free(pathname);
Sprawdzamy, czy użytkownik wprowadził poprawny numer klienta:
result = dvd_member_get_id_from_number(member_no, &member_id);
if (result != DVD_SUCCESS)
{
dvd_gui_show_result(_("The member number is not valid"), result);
return;
}
Teraz tworzymy okno raportu wypożyczeń i wypełniamy dane dotyczące klienta w oknie dialogowym:
rent_report_dialog = create_rent_report_dialog();
rent_result_clist = GTK_CLIST(lookup_widget(rent_report_dialog,
"rent_result_clist"));
dvd_member_get(member_id, &member);
LABEL_SET_TEXT(title);
LABEL_SET_TEXT(fname);
LABEL_SET_TEXT(lname);
LABEL_SET_TEXT(house_flat_ref);
LABEL_SET_TEXT(address1);
LABEL_SET_TEXT(address2);
gtk_frame_set_label(GTK_FRAME(lookup_widget(rent_report_dialog,
"member_frame")), g_strdup_printf("Member %s", member_no));
Następnie anulujemy wszystkie rezerwacje, których mogli dokonać klienci:
dvd_gui_show_result("dvd_reserve_title_cancel",
dvd_reserve_title_cancel(member_id));
Teraz dla każdego identyfikatora tytułu przechowywanego w liście próbujemy wykonać operację wypożyczenia, dodając wpisy do raportu wypożyczenia GtkClist.
count = g_slist_length(rent_disk_slisl);
while (count--)
{
title_id = GPOINTER_TO_INT(g_slist_nth_data(rent_disk_slist, count));
dvd_title_get(title_id, &title);
text[0] = g_strdup_printf("%d, title_id);
text[1] = g_strdup_printf("%s", title.title_text);
result = dvd_rent_title(member_id, titleid, &disk_id);
Jeżeli operacja wypożyczenia udała się, to do listy clist dodajemy identyfikator płyty wypożyczonej klientowi i wpisujemy komunikat do logu. Jeżeli wypożyczenie się nie udało, to pole z identyfikatorem płyty pozostawiamy puste.
if (result == DVD_SUCCESS)
{
text[3] = g_strdup_printf("%d", disk_id);
msg = g_strdup_printf(_("Rented disk %d to Member: %s"), disk_id,
member_no);
add_log_message(msg);
g_free(msg);
}
else
text[3] = "";
Następnie wstawiamy wstępnie wiersz do listy clist i dodajemy odpowiednią mapę bitową. Zwróćmy uwagę, że tekst musi być wstawiony do nowego wiersza przed dodaniem mapy.
gtk_clist_prepend(rent_result_clist, text);
if (result == DVD_SUCCESS)
gtk_clist_set_poxmap(rent_result)clist, 0, 2, tick, tick_mask);
else
gtk_clist_set_pixmap(rent_result_clist, 0, 2, cross, cross_mask);
}
Na zakończenie ustalamy wysokość wiersza, wywołujemy funkcję gnome_dialog_set_close powodującą usunięcie okna dialogowego po kliknięciu dowolnego przycisku i pokazujemy okno.
gtk_clist_set_row_height(rent_result_clist, PIXMAP_HEIGHT);
gnome_dialog_set_close(GNOME_DIALOG(rent_report_dialog),
TRUE);
gtk_widget_show(rent_report_dialog);
}
}
on_rent_dvd_dialog_add_clicked
Jest to funkcja wywołania zwrotnego podłączona do sygnału „clicked”, pochodzącego z przycisku Add w oknie dialogowym. Jej zadaniem jest odczyt identyfikatora tytułu wprowadzonego do GtkSpinButton w oknie dialogowym wypożyczania. Jeżeli ten identyfikator jest poprawny, to następuje dopisanie go do listy tytułów do wypożyczenia (rent_disk_slist) i modyfikacja GtkClist w celu uwzględnienia dokonanej zmiany.
void
on_rent_dvd_dialog_add_clicked (GtkButton *button,
gpointer user_data)
{
GtkCList *disk_clist;
GtkWidget *titleid_spinbutton;
gint titleid;
dvd_title title;
disk_clist = GTK_CLIST(lookup_widget(GTK_WIDGET(button),
"rent_dvd_dialog_disk_clist"));
titleid_spinbutton = lookup_widget(GTK_WIDGET(button),
"titleid_spinbutton");
titleid = gtk_spin_button_get_value_as_int
(GTK_SPIN_BUTTON(titleid_spinbutton));
if (dvd_title_get(titleid, &title) == DVD_SUCCESS)
{
rent_disk_slist = g_slist_append(rent_disk_slist,
GINT_TO_POINTER(titleid));
update_rent_dvd_diskid_clist(disk_clist);
}
}
on_rent_dvd_dialog_remove_clicked
Funkcja ta odpowiada na kliknięcie przycisku Remove, usuwając z listy ostatnio wprowadzony identyfikator tytułu. Do pobrania zerowej pozycji z listy rent_disk_slist, która następnie będzie usunięta przez g_slist_remove, użyto tutaj g_slist_nth_data.
W rzeczywistości nie ma potrzeby wykonywania konwersji danych typu gpointer na int i odwrotnie między kolejnymi wywołaniami, ale czyniąc tak, chcemy podkreślić fakt, że lista zawiera identyfikatory tytułów.
void
on_rent_dvd_dialog_remove_clicked (GtkButton *button
gpointer user_data)
{
gint titleid;
titleid = GPOINTER_TO_INT(g_slist_nth_data(rent_disk_slist, 0));
rent_disk_slist = g_slist_remove(rent_disk_slist, GINT_TO_POINTER(titleid));
update_rent_dvd_diskid_clist(disk_clist);
}
update_rent_dvd_diskid_clist
Ta ostatnia funkcja w rent_dialog.c aktualizuje GtkClist w oknie dialogowym wypożyczania:
void
update_rent_dvd_diskid_clist(GtkCList *disk_clist)
{
gchar *text[2];
dvd_title title;
gint title_id;
gint count;
count = g_slist_length(rent_disk_slist);
gtk_clist_clear(disk_clist);
while (count--) {
title_id = GPOINTER_TO_INT (g_slist_nth_data(rent_disk_slist, count));
if (dvd_title_get(title_id, &title) == DVD_SUCCESS)
{
text[0] = g_strdup_printf("%d", title_id);
text[1] = title.title_text;
gtk_clist_prepend(disk_clist, text);
}
}
}
search_window.c
Okno wyszukiwania jest najbardziej skomplikowanym oknem w aplikacji dvdstore. Jest to okno typu GtkWindow zawierające widżet GtkNotebook, który ma trzy strony służące do wyszukiwania tytułów, klientów i płyt. Zawiera ono także pasek statusu, przycisk służący do oczyszczania zawartości i kontekstowe menu.
Chcąc utrzymać w książce zwartość cytowanej treści kodu źródłowego i ułatwić jego śledzenie, podajemy tylko fragmenty podobne do już omawianych, nie dodając niczego nowego. Przykładowo: trzy funkcje update_title_search_clist, update_member_search_clist i update_disk_search_clist wykonują równoważne operacje modyfikujące zawartość clist na podstawie wyniku wyszukiwania.
Najpierw zdefiniujemy dwie zmienne typu enum ułatwiające czytanie kodu: search_page umożliwia odniesienie do zakładek GtkNotebook poprzez nazwę, a nie numer, podobnie jak member_search_type odzwierciedla stan GtkOptionMenu użytego wyboru sposobu wyszukiwania klienta (na podstawie jego numeru albo nazwiska).
enum search_page {
TITLE_PAGE,
MEMBER_PAGE,
DISK_PAGE };
enum _member_search_type {
MEMBER_NO,
LAST_NAME }
member_search_type;
Numer wiersza wybranego w GtkClist jest przechowywany w selected_row i korzysta z niego menu kontekstowe. Listy tytułów, klientów i płyt (typu slist) są używane do przechowywania odpowiadających im wyników wyszukiwania, czyli identyfikatorów tytułu, klienta i płyty jako wartości typu int:
static gint selected_row;
GSList *title_search_slist;
GSList *member_search)slist;
GSList *disk_search_slist;
void
do_search_dialog()
{
GtkWidget *member_optionmenu;
GtkWidget *member_menu;
Nie chcemy utracić wyników wyszukiwania po zamknięciu okna search_window, więc w takim przypadku będziemy je ukrywać, a nie usuwać:
if (search_window != NULL)
{
gtk_widget_show(search_window);
}
else
{
/* Wywołanie funkcji utworzonej w Glade do zbudowania okna,
* ustawienia menu opcji i podłączenia wywołań zwrotnych
*/
search_window = create_search_window ();
Następnie ustawiamy jako GtkOptionmenu menu member_optionmenu zbudowane za pomocą Glade:
member_optionmenu = lookup_widget(search_window, "member_optionmenu");
member_menu = create_member_optionmenu();
gtk_option_menu_remove_menu (GTK_OPTION_MENU(member_optionmenu));
gtk_option_menu_set_menu(GTK_OPTION_MENU(member_optionmenu),
member_menu);
gtk_signal_connect(GTK_OBJECT(search_window), "delete_event",
GTK_SIGNAL_FUNC(gtk_widget_hide), &search_window);
gtk_window_set_default_size(GTK_WINDOW(search_window), 500, 500);
update_search_window_preferences();
gtk_widget_show (search_window);
}
}
update_title_search_clist
Poniżej pokazano pierwszą z trzech funkcji odświeżających zawartość GtkClist. Zmienna count przechowuje długość listy clist, której zawartość jest wypełniana wierszami w pętli:
void
update_title_search_clist()
{
dvd_title title;
GtkCList *clist;
gintcount;
gint id;
gchar *text[10];
count = g_slist_length(title_search_slist);
clist = GTK_CLIST(lookup_widget(search_window,
"title_search_clist"));
gtk_clist_clear(clist);
gtk_clist_freeze (clist);
while (count--) {
id = GPOINTER_TO_INT (g_slist_nth_data(title_search_slist, count));
dvd_title_get(id, &title);
text[0] = g_strdup_printf("%d", id);
text[1] = title.title_text;
text[2] = title.asin;
text[3] = title.director;
text[4] = title.genre;
text[5] = title.classification;
text[6] = title.actor1;
text[7] = title.actor2;
text[8] = title.release_date;
text[9] = title.rental_cost;
gtk_clist_prepend(clist, text);
}
gtk_clist_thaw (clist);
}
Jak już wspomniano wcześniej, update_member_search_list i update_disk_search_list mają podobną postać.
Funkcja on_search_clicked stanowi właściwą treść pliku search_wicdow.c i jest wywoływana po kliknięciu przycisku Search. Najpierw funkcja ta znajduje w GtkNotebook właściwą otwartą zakładkę, aby stwierdzić, czy będzie szukany tytuł, klient czy też płyta.
void
on_search_clicked (GtkButton *button,
gpointer user_data)
{
GtkWidget *entry1, *entry2, *menu, *active_item, *member_optionmenu;
gchar *entry1_text;
gchar *entry2_text;
gchar member_no[6];
gchar *appbar_text = NULL;
gint diskid, current_page, id, count, member_search_type;
gint i = 0;
gint *ids;
current_page = gtk_notebook_get_current_page(
GTK_NOTEBOOK (
lookup_widget(GTK_WIDGET (button),
"search_notebook")
)
);
Jeżeli otwarta jest zakładka Title, to rozpoczyna się szukanie tytułu. Zawartość dwóch widżetów GnomeEntry jest pobierana z zakładki i przekazywana do funkcji dvd_title_search:
if (current_page == TITLE_PAGE)
{
/* Wyszukiwanie tytułu filmu DVD */
entry1 = lookup_widget(GTK_WIDGET (button), "title_entry");
entry1_text = gtk_entry_get_text(
GTK_ENTRY(gnome_entry_gtk_entry(GNOME_ENTRY(entry1)))
);
entry2 = lookup_widget(GTK_WIDGET (button), "actor_entry");
entry2_text = gtk_entry_get_text(
GTK_ENTRY(gnome_entry_gtk_entry(GNOME_ENTYR(entry2)))
);
dvd_gui_show_result("dvd_title_search", dvd_title_search(entry1_text,
entry2_text, &ids, &count));
Następnie wynik przeszukiwania zostaje podsumowany na pasku statusu w oknie wyszukiwania:
appbar_text = g_printf(
_("Found %d result(s) searching
for" \"%s\" with Actor/Director %s"),
count, entry1_text, entry2_text
);
gnome_appbar_set_status(GNOME_APPBAR (
lookup_widget(GTK_WIDGET(button), "search_appbar")
),
appbar_text
);
Teraz lista wyszukanych tytułów jest oczyszczana z poprzedniej zawartości i dopisywane są do niej identyfikatory znalezionych tytułów. Aby wyświetlić wynik, należy wywołać funkcję update_title_search_clist:
g_slist_free(title_search_slist);
title_search_slist = NULL;
while (count--)
title_search_slist = g_slist_append(title_search_slist,
GINT_TO_POINTER(ids[i++]));
update_title_search_clist();
free(ids);
}
if (current_page == MEMBER_PAGE)
{
Kolejne cztery wiersze kodu służą do pobrania aktualnie wybranej pozycji z menu GtkOptionmenu. Niestety, używamy dość prymitywnej metody, ponieważ nie mamy tu stosownego API:
member_optionmenu = lookup_widget(GTK_WIDGET(button),
"member_optionmenu");
menu = GTK_OPTION_MENU(member_optionmenu)->menu;
active_item = gtk_menu_active(GTK_MENU(menu));
member_search_type = g_list_index(GTK_MENU_SHELL(menu)->children,
active_item);
entry1 = lookup_widget(GTK_WIDGET (button), "member_entry");
entry1_tect = gtk_entry_get_text(GTK_ENTRY(gnome_entry_gtk_entry(
GNOME_ENTRY(entry1))));
g_slist_free(member_search_slist);
member_search_slist = NULL;
Jeśli trzeba byłoby wyszukać szczegółowe informacje o kliencie na podstawie jego numeru, to po prostu należałoby wywołać funkcję dvd_member_get_id_from_number. Jeśli to wywołanie się powiedzie, identyfikator klienta zostanie dopisany do listy. Wyszukiwanie klienta na podstawie numeru może się zakończyć tylko pozytywnym wynikiem.
if (member_search_type == MEMBER_NO)
{
strncpy(member_no, entry1_text, 6);
if (dvd_member_get_id_from_number (member_no, &id) == DVD_SUCCESS)
member_search_list = g_slist_append(member_search_slist,
GINT_TO_POINTER(id));
appbar_text = g_strdup_printf("Found 1 result searching for
\"%s\"", entry1_text);
gnome_appbar_set_status(GNOME_APPBAR (lookup_widget(
GTK_WIDGET (button), "search_appbar")), appbar_text);
}
else
{
dvd_gui_show_result("member_search"
dvd_member_search(entry1_text, &ids, &count));
appbar_text = g_strdup_printf("Found %d result(s) searching for
\"%s"", count, entry1_text);
gnome_appbar_set_status(GNOME_WIDGET (button), "search_appbar")
), appbar_text);
while (count--)
member_search_slist = g_slist_append(member_search_slist,
GINT_TO_POINTER(ids[i++]));
}
update_member_search_clist();
if (member_search_type == LAST_NAME)
free(ids);
}
Przy wyszukiwaniu płyty pokazywane są wszystkie płyty związane z danym tytułem, a jeśli któraś z nich została wypożyczona, wtedy wyświetla się numer klienta, który ją wypożyczył.
if (current_page == DISK_PAGE)
{
entry1 = lookup_widget(GTK_WIDGET (button), "diskid_spinbutton");
diskid = gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (entry1));
clist = GTK_CLIST(lookup_widget(GTK_WIDGET(button),
"disk_search_clist"));
dvd_gui_show_result("disk_search",
dvd_disk_search(diskid, &ids, &count));
g_slist_free(disk_search_slist);
disk_search_slist = NULL;
appbar_text = g_strdup_printf("Found %d Disk(s) for Title ID %d",
count, diskid );
gnome_appbar_set_status(GNOME_APPBAR (
lookup_widget(GTK_WIDGET (button), "search_appbar")
), appbar_text);
while (count--)
disk_search_slist = g_slist_append(disk_search_slist,
GINT_TO_POINTER(ids[i++]));
update_disk_search_clist();
}
g_free(appbar_text);
}
on_search_close_clicked
void
on_search_close_clicked (GtkButton *button,
gpointer user_data)
{
gtk_widget_hide(search_window);
}
on_search_clear_clicked
Funkcja ta oczyszcza zawartość listy clist na aktualnie otwartej zakładce:
void
on_search_close_clicked (GtkButton *button,
gpointer user_data)
{
gint current_page;
GtkWidget *search_notebook;
GtkWidget *clist;
search_notebook = lookup_widget (GTK_WIDGET (button), "search_notebook");
current_page = gtk_notebook_get_current_page (
GTK_NOTEBOOK(search_notebook));
switch (current_page)
{
case TITLE_PAGE:
clist = lookup_widget (GTK_WIDGET (button), "title_search_clist");
break;
case MEMBER_PAGE:
clist = lookup_widget (GTK_WIDGET (button), "member_search_clist");
break;
case DISK_PAGE:
clist = lookup_widget (GTK_WIDGET (button), "disk_search_clist");
break;
default:
g_assert_not_reached();
}
gtk_clist_clear (GTK_CLIST (clist));
gnome_appbar_set_status(GNOME_APPBAR (
lookup_widget(GTK_WIDGET (button), "search_appbar")
), "Cleared");
}
on_dvd_search_clist_button_press_event
Ta funkcja wyświetla odpowiednie menu kontekstowe po naciśnięciu prawego klawisza myszy wewnątrz listy clist zawierającej tytuły lub klientów.
gboolean
on_dvd_search_clist_button_press_event (GtkWidget *widget,
GdkEventButton *event,
gpointer user_data)
{
GtkWidget *menu;
GtkCList *clist;
gint row, column;
g_return_val_if_fail(widget != NULL, FALSE);
menu = create_dvd_popup_menu();
if (event->type == GDK_BUTTON_PRESS)
{
GdkEventButton *buttonevent = (GdkEventButton *) event;
if ( buttonevent->button == GDK_BUTTON1_MASK )
{
clist = GTK_CLIST(widget);
if (gtk_clist_get_selection_info(clist,
buttonevent->x,
buttonevent->y,
&row,
&column)) {
gtk_clist_select_rows(clist, row, column);
selected_row = row;
gtk_menu_popup ( GTK_MENU (menu), NULL, NULL, NULL, NULL,
buttonevent->button, 0 );
return TRUE;
}
}
}
return FALSE;
}
Pozostałe funkcje są wywołaniami zwrotnymi dla czterech pozycji menu kontekstowego: Rent, Reserve, Edit oraz Delete.
on_search_menu_rent_activate
Funkcja ta stwierdza, czy otwarta jest zakładka dla tytułów, czy dla klientów; następnie wywołuje do_rent_dvd_dialog, przekazując jako argument identyfikator tytułu lub klienta dla wybranego wiersza:
void
on_search_menu_rent_activate (GtkMenuItem *menuitem,
gpointer user_data)
{
gint current_page;
gint id;
dvd_store_member member;
current_page = gtk_notebook_get_current_page (GTK_NOTEBOOK
(lookup_widget(search_window, "search_notebook")));
if (current_page == TITLE_PAGE)
{
g_return_if_fail (title_search_slist != NULL);
id = GPOINTER_TO_INT(G_slist_nth_data(title_search_slist,
selected_row));
do_rent_dvd_dialog(NULL, id);
}
if (current_page == MEMBER_PAGE) {
g_return_if_fail (member_search_slist != NULL);
id = GPOINTER_TO_INT(g_slist_nth_data(member_search_slist,
selected_row));
dvd_member_get(id, &member);
do_rent_dvd_dialog(member.member_no, 0);
}
}
on_search_menu_edit_activate
void
on_search_menu_edit_activate (GtkMenuItem *menuitem,
gpointer user_data)
{
gint current_page;
gint id;
g_return_if_fail (search_window != NULL);
current_page = gtk_notebook_get_current_page (GTK_NOTEBOOK
(lookup_widget(search_window, "search_notebook")));
if (current_page == TITLE_PAGE)
{
g_return_if_fail (title_search_slist != NULL);
id = GPOINTER_TO_INT(g_slist_nth_data(title_search_slist,
selected_row));
do_dvd_dialog(id);
}
if (current_page == MEMBER_PAGE)
{
g_return_if_fail (member_search_slist != NULL);
id = GPOINTER_TO_INT(g_slist_nth_data(member_search_slist,
selected_row));
do_member_dialog(id);
}
}
on_search_menu_delete_activate
Funkcja delete_activate żąda od użytkownika potwierdzenia zamiaru usunięcia wybranej pozycji z listy tytułów lub klientów, wyświetlając okno dialogowe Gnome:
void
on_search_menu_delete_activate (GtkMenuItem *menuitem,
gpointer user_data)
{
GtkWidget *dialog;
gint id;
gint reply;
gint current_page;
g_return_if_fail (search_window != NULL);
current_page = gtk_notebook_get_current_page (GTK_NOTEBOOK
(lookup_widget(search_window, "search_notebook")));
if (current_page == TITLE_PAGE)
{
g_return_if_fail (title_search_slist != NULL);
id = GPOINTER_TO_INT(g_slist_nth_data(title_search_slist,
selected_row));
dialog = gnome_message_box_new(_("Delete this Title?"),
GNOME_MESSAGE_BOX_QUESTION,
GNOME_STOCK_BUTTON_YES,
GNOME_STOCK_BUTTON_NO,
NULL);
gtk_widget_show(dialog);
reply = gnome_dialog_run(GNOME_DIALOG(dialog));
if (reply == GNOME_OK) {
dvd_title_delete(id);
title_search_slist = g_slist_remove (title_search_slist,
GINT_TO_POINTER(id));
update_title_search_clist();
}
}
if (current_page == MEMBER_PAGE)
{
g_return_if_fail (member_search_slist != NULL);
id = GPOINTER_TO_INT(g_slist_nth_data(member_search_slist,
selected_row));
dialog = gnome_message_box_new(_("Delete this Member?"),
GNOME_MESSAGE_BOX_QUESTION,
GNOME_STOCK_BUTTON_OK,
GNOME_STOCK_BUTTON_CANCEL,
NULL);
reply = gnome_dialog_run(GNOME_DIALOG(dialog));
if (reply == GNOME_OK)
{
dvd_member_delete(id);
member_search_slsist = g_slist_remove (member_search_slist,
GINT_TO_POINTER(id));
update_member_search_clist();
}
}
}
on_search_menu_reserve_activate
void
on_search_menu_reserve_activate (GtkMenuItem *menuitem,
gpointer user_data)
{
gint current_page;
gint id;
g_return_if_fail (search_window != NULL);
current_page = gtk_notebook_get_current_page (GTK_NOTEBOOK
(lookup_widget(search_window, "search_notebook")));
if (current_page == TITLE_PAGE)
{
g_return_if_fail (title_search_slist != NULL);
id = GPOINTER_TO_INT(g_slist_nth_data(title_search_slist,
selected_row));
do_reserve_dialog(0, id);
}
if (current_page == MEMBER_PAGE)
{
g_return_if_fail (member_search_slist != NULL);
id = GPOINTER_TO_INT(g_slist_nth_data(member_search_slist,
selected_row));
do_reserve_dialog(id, 0);
}
}
misc.c
Ostatnim plikiem źródłowym, który tu omówimy, jest misc.c zawierający różne funkcje pomocnicze. Tutaj inicjujemy deskryptor pliku zawierającego log oraz zmienną typu gboolean opisującą stan połączenia z bazą danych. Makrodefinicja SET_SENSITIVE służy do ustawiania stanu aktywności widżetów zgodnie z wartością zmiennej sensitive.
static FILE *logfile;
static gboolean connected = FALSE;
#define SET_SENSITIVE(widget)
gtk_widget_set_sensitive(GTK_WIDGET(
lookup_widget(main_window, #widget)
), sensitive)
dvd_store_connect
Funkcja ta obsługuje potwierdzanie tożsamości użytkownika i jego logowanie do bazy. Zmienna user jest zmienną globalną, której wartość jest pobierana z wiersza poleceń, jeżeli była użyta opcja --username:
void
dvd_store_connect()
{
GtkWidget *login_dialog;
GtkWidget *gtk_username_entry;
gchar *msg;
gint reply;
gint result;
Jeśli opcja --username nie była użyta, to zmienna user ma wartość NULL i wówczas tworzone jest okno dialogowe logowania. Potrzebne dane dotyczące nazwy użytkownika i hasła są wówczas pobierane z tego okna:
if (!user)
{
login_dialog = create_login_dialog();
gnome_dialog_set_default(GNOME_DIALOG(login_dialog), GNOME_OK);
gnome_dialog_editable_enters(GNOME_DIALOG(login_dialog),
GTK_EDITABLE(lookup_widget(login_dialog,
"password")));
reply = gnome_dialog_run(GNOME_DIALOG(login_dialog));
if (reply != GNOME_OK)
{
gtk_widget_destroy(login_dialog);
return;
}
gtk_username_entry = gnome_entry_gtk_entry(GNOME_ENTRY(
lookup_widget(login_dialog, "username")
));
user = g_strdup(gtk_entry_text(GTK_ENTRY(gtk_username_entry)));
passwd = g_strdup(gtk_entry_get_text(GTK_ENTRY(
lookup_widget(login_dialog, "password")
));
gtk_widget_destroy(login_dialog);
}
result = dvd_open_db_login(user, passwd);
Jeżeli połączenie się udało, to nastąpi zmiana stanu aktywności elementów menu i paska narzędziowego oraz zmiana zawartości paska statusu, a w logu pojawi się komunikat:
if (result == DVD_SUCCESS)
{
connected = TRUE;
sensitize_widgets();
gnome_appbar_push(GNOME_APPBAR(lookup_widget (main_window, "appbar1")),
_("Connected"));
msg = g_strdup_printf(_("User %s connected to the database"), user);
add_log_message(msg);
g_free(msg);
}
else
{
dvd_gui_show_result(_("Cannot connect to the database. Check the
Username and\n password are correct and that
the database is set up correctly"), 0);
user = NULL;
passwd = NULL;
}
}
dvd_store_disconnect
Funkcja ta zajmuje się nieco prostszą czynnością niż poprzednia, a mianowicie — rozłączaniem:
void
dvd_store_disconnect()
{
g_return_if_fail(connected);
dvd_close_db();
connected = FALSE;
user = NULL;
passwd = NULL;
sensitize_widgets();
gnome_appbar_push(GNOME_APPBAR(lookup_widget (main_window,
"appbar1")),_("Not Connected"));
add_log_message(_("Disconnected from the database"));
}
sensitize_widgets
Funkcja ta zmienia na przeciwny stan aktywności pozycji menu i elementów paska narzędziowego dotyczących bazy danych. Jeżeli nie ma połączenia z bazą, wówczas mają one szary kolor i nie reagują na kliknięcia myszą.
void
sensitize_widgets()
{
static gboolean sensitive = FALSE;
SET_SENSITIVE(menu_disconnect);
SET_SENSITIVE(add_member);
SET_SENSITIVE(new_title);
SET_SENSITIVE(new_disk);
SET_SENSITIVE(menu_search);
SET_SENSITIVE(rent_dvd);
SET_SENSITIVE(return_dvd);
SET_SENSITIVE(reserve);
SET_SENSITIVE(disconnect_button);
SET_SENSITIVE(rent_button);
SET_SENSITIVE(return_button);
SET_SENSITIVE(add_member_button);
SET_SENSITIVE(search_button);
SET_SENSITIVE(reserve_button);
gtk_widget_set_sensitive (GTK_WIDGET(lookup_widget(main_window,
"connect_button")), !sensitive);
gtk_widget_set_sensitive (GTK_WIDGET(lookup_widget(main_window,
"menu_connect")), !sensitive);
sensitive = !sensitive;
}
exit_dvdstore
Ta funkcja obsługuje zamykanie aplikacji. Jest ona wywoływana po odbiorze sygnału delete_event. Następuje sprawdzenie, czy rzeczywiście aplikacja ma zostać zamknięta. Jeśli tak, wtedy następuje poprawne odłączenie od bazy. Warto zauważyć, że zwracana wartość nie jest intuicyjnie rozpoznawalna — ponieważ wynosi ona FALSE w przypadku potwierdzenia chęci zamknięcia aplikacji.
gboolean
exit_dvdstore(void)
{
GtkWidget *dialog;
gint reply;
dialog = gnome_message_box_new("Are you sure you want to quit?"),
GNOME_MESSAGE_BOX_QUESTION,
GNOME_STOCK_BUTTON_YES,
GNOME_STOCK_BUTTON_NO,
NULL);
reply = gnome_dialog_run(GNOME_DIALOG(dialog));
if (reply != GNOME_OK)
return TRUE;
if (connected)
dvd_store_disconnect();
gtk_main_quit ();
return FALSE;
}
open_log_file
Ta funkcja otwiera plik logu w celu jego modyfikacji. Nazwa tego pliku jest ustalana w oknie konfiguracyjnym Preferences i jest przechowywana w zmiennej logfile_name:
void
open_log_file(void)
{
if ((logfile = fopen(gnome_config_get_string_with_default(
"/dvdstore/general/logfile_name=logfile.txt", NULL) ,"a")) == NULL)
{
dvd_gui_show_result(_("Cannot open logfile"), 0);
}
}
add_log_message
Ta funkcja dopisuje komunikat msg do okna wyświetlającego log oraz dodaje go do pliku z logiem, wpisując także bieżącą datę i czas:
void
add_log_message(gchar *msg)
{
GtkText *textbox;
gchar *text;
gchar time_text[50];
struct tm *time_now = NULL;
time_t the_time;
textbox = GTK_TEXT(lookup_widget(main_window, "text_box"));
the_time = time(NULL);
time_now = localtime(&the_time);
strtime(time_text, sizeof(time_text), "%R %x", time_now);
text = g_strdup_printf("%s -- %s\n", time_text, msg);
gtk_text_insert(textbox, NULL, NULL, NULL, text, -1);
if (logfile != NULL)
fprintf(logfile, %s -- %s\n", time_text, msg);
}
dvd_gui_show_result
Jest to dość prymitywna funkcja obsługująca błędy. Jeżeli numerem błędu będzie DVD_SUCCESS, to na standardowe wyjście jest wysyłany komunikat msg; w przeciwnym wypadku pojawi się ostrzegawcze okno dialogowe zawierające komunikat msg i treść komunikatu o błędzie:
void
dvd_gui_show_result(char *msg, int err)
{
gchar *err_msg;
gchar *dialog_text;
GtkWidget *dialog;
(void) dvd_err_text(err, &err_msg);
if (err == DVD_SUCCESS)
g_print("%s: %s\n", msg, err_msg);
else
{
dialog_text = g_strdup_printf(_("DVDStore Error:\n %s: %s"),
msg, err_msg);
dialog = gnome_message_box_new(dialog_text,
GNOME_MESSAGE_BOX_WARNING,
GNOME_STOCK_BUTTON_OK,
NULL);
gtk_widget_show(dialog);
}
}
date_overdue
Funkcja ta zwraca liczbę całkowitą pokazującą, czy nie minął termin zwrotu płyty wypożyczonej danego dnia. Uzyskanie takiej informacji mogłoby być dość kłopotliwe, gdyby nie funkcje kalendarza występujące w bibliotece glib, bowiem nie można w prosty sposób dodać liczby dni do daty i porównać ją z jakąś inną datą, uwzględniając przy tym różne długości miesięcy i lata przestępne. Wykorzystując bibliotekę glib, zmieniamy każdą datę na obiekt GDate (za pomocą sscanf), dodajemy liczbę dozwolonych dni wypożyczenia (za pomocą g_date_add_days) i następnie porównujemy daty używając g_date_compare:
gint
date_overdue(gchar *date)
{
gchar *date_today;
gint day, month, year;
gintoverdue;
GDate *g_rent_date;
GDate *g_date_today;
dvd_today(&date_today);
sscanf(date_today, "%04d%02d%02d", &year, &month, &day);
g_date_today = g_date_new_dmy(day, month, year);
sscanf(date, "%04d%02d%02d", &year, &month, &day);
g_rent_date = g_date_new_dmy(day, month, year);
g_date_add_days(g_rent_date, gnome_config_get_int_with_default
("/dvdstore/general/days_rent=3", NULL));
/* g_date_compare zwraca zero dla równych dat, wartość ujemną jeśli
* g_rent_date jest mniejsze niż g_date_today i wartość dodatnią jeśli
* g_rent_date jest większe niż g_date_today
*/
overdue = g_date_compare(g_rent_date, g_date_today);
g_date_free(g_rent_date);
g_date_free(g_date_today);
return overdue;
}
do_about_dialog
Na zakończenie mamy funkcję wyświetlającą okno informacyjne „about”. Zauważmy, że okna tego rodzaju tworzone przez Glade są oknami modalnymi:
void
do_about_dialog()
{
GtkWidget* about_dialog = NULL;
about_dialog = create_about_dialog ();
gtk_widget_show (about_dialog);
}
Na tym kończy się przegląd kodu źródłowego dvdstore.
Podsumowanie
W tym rozdziale zapoznaliśmy się szczegółowo z Glade i zobaczyliśmy, jak tworzy się przykładową aplikację. Pokazano tu wiele właściwości Glade oraz możliwości szybkiego tworzenia skomplikowanych aplikacji na podstawie szkieletu wygenerowanego przez ten program i połączonego z prostymi, lecz wydajnymi bibliotekami GNOME/GTK+.
Pokazaliśmy metodę lookup_widget służącą do pobierania wskaźnika i zastosowaliśmy libglade do prostej przykładowej aplikacji. Następnie zapoznaliśmy się z budową interfejsu graficznego używanego przez aplikację obsługującą wypożyczalnię płyt DVD, w którym zastosowano funkcje GNOME/GTK+.
11 Część I ♦ Podstawy obsługi systemu WhizBang (Nagłówek strony)
22 H:\Książki\!Wit\Zaawansowane programowanie w systemie Linux\6 po jezykowej\R-09-06.doc
R-09-06.doc Strona 1z 1
1