13. Programowanie GUI za pomocą KDE/Qt
Wprowadzenie
System X Window oferuje programistom do wyboru rozliczne biblioteki widżetów. Przez długi czas dominującym zestawem narzędzi był komercyjny Motif. Motif jednakowoż nie był dostępny za darmo. Powstały nowe zestawy narzędzi, wykorzystujące zwłaszcza coraz powszechniejsze języki zorientowane obiektowo (ang. object-oriented languages). Dziś dostępnych jest mnóstwo zestawów narzędzi, a większość z nich dostępna jest nieodpłatnie. Jednym z najbardziej popularnych pakietów narzędziowych jest Qt i to jemu przyjrzymy się bliżej w tym rozdziale. Później zajmiemy się środowiskiem pulpitowym K (ang. K Desktop Environment), w skrócie KDE, zbudowanym na bazie Qt. KDE/Qt jest alternatywą GNOME/GTK+.
Od Czytelnika oczekuje się biegłej znajomości C++.
Informacje o Qt
Qt jest łatwo przenośną komponentą C++ graficznego interfejsu użytkownika GUI. Qt jest dostępny nie tylko dla Linuksa, ale również dla tuzina innych wariantów UNIX-a, a także dla Microsoft Windows. Uzyskiwanie licencji na wykorzystanie Qt oraz ocena tego, czy jest to produkt autentycznie nieodpłatny wiązało się w przeszłości z licznymi problemami. Zatem tytułem wyjaśnienia, Qt dla UNIX-a jest za darmo wtedy, gdy służy on do pisania darmowego oprogramowania z otwartym dostępem do kodu źródłowego, a kod źródłowy dla wersji UNIX-owej jest dostępny nieodpłatnie na warunkach licencji publicznej Qt QPL (ang. Qt Public License). Firma Trolltech, firmująca Qt, jest bardzo przychylna społeczności zwolenników otwartego dostępu do kodu źródłowego. Otóż, zapewniła temu środowisku na wypadek swego bankructwa korzystanie z Qt na warunkach licencji systemu operacyjnego Berkeley UNIX BSD (ang. Berkeley Software Distribution). Z ogromną społecznością wspierającą otwarty dostęp do oprogramowania KDE i Qt bezpieczna przyszłość Qt jest zapewniona. Qt z powodu swoich niejasności licencyjnych był obiektem pewnych kontrowersji. Te jednak ucichły wraz z pojawieniem się Qt 2.x, który zapoczątkował licencję QLP. Wszyscy zaniepokojeni statusem prawnym swego programu powinni przeczytać licencję, by upewnić się, czy są usatysfakcjonowani jej warunkami.
Z technicznego punktu widzenia Qt jawi się jeszcze atrakcyjniej. O jego właściwości łatwego przenoszenia (ang. portability) już wspomniano. Co więcej, ten zestaw narzędziowy jest autentycznie zorientowany obiektowo i tworzenie przy pomocy Qt komponent programowych wielokrotnego użytku przychodzi całkiem naturalnie. Qt oferuje wszystkie znane z szablonu GUI standardowe elementy interfejsu użytkownika, takie jak przyciski (ang. push buttons), listy, menu, pola wyboru (ang. check boxes). Jednak piękno Qt tkwi w łatwości tworzenia tego, czego Qt nie posiada. Specjalne, czy też przystosowane do indywidualnych potrzeb wersje dostępnych elementów interfejsu użytkownika na przykład, są niezbędne w większości aplikacji. A daje się to łatwo i naturalnie osiągnąć za pomocą Qt.
Kolejna cechą Qt jest jego obsługa dostosowanego dla potrzeb użytkownika wyglądu i działania, czy wzornictwa pulpitu (ang. themes). Ale Qt to nie tylko obsługa GUI. Umiędzynarodowienie (ang. internationalization), klasy użytkowe (ang. utility classes) takie jak listy i słowniki, klasy zarządzania plikami, klasy wyrażeń regularnych i kilka innych funkcji — to wszystko dostarczy Qt.
Jak wiadomo, doskonałość techniczna niekoniecznie oznacza sukces. Sukces Qt tkwi też w wyśmienitej dokumentacji, która rzadko pozostawia luki. A jeśli nawet nie znajdzie się tam odpowiedzi na dręczące nas pytanie, to zawsze można przyjrzeć się kodowi źródłowemu i skorzystać z bardzo przyjaznej i pomocnej listy korespondencyjnej (ang. mailing list).
Informacje o KDE
KDE jest potężnym, bezpłatnym pulpitem dla UNIX-a, z otwartym dostępem do kodu źródłowego. KDE, podobnie jak wiele innych programów z otwartym dostępem do kodu źródłowego jest projektem internetowym, którego twórcy rozproszeni są po całym świecie. Pomysł zrodził się na skutek tego, że jeden programista, Matthias Ettrich, zaproponował nieodpłatne środowisko pulpitowe dla Linuksa. Propozycja pojawiła się wkrótce po tym, jak Trolltech wydał pierwszą wersję Qt. Ettrich przyjrzał się bibliotece Qt i zaproponował jej wykorzystanie ze względu na jej wysoką jakość i przyjazną, otwartą formę licencji. To doprowadziło w październiku 1996 roku do utworzenia KDE. Nazwa KDE jest skrótem od K Desktop Environment (środowisko pulpitowe K), przy czym „K” nie znaczy nic. Obecnie kilkuset programistów i ponad stu tłumaczy bierze udział w projekcie KDE.
KDE jest interesujący zarówno dla programistów jak i użytkowników. Użytkownikom odpowiada zapewne to, że KDE wymusza spójność (ang. consistency) między aplikacjami. Użytkownik w znacznym stopniu może centralnie kierować ustawieniami wyglądu aplikacji KDE. Programistom natomiast, KDE oferuje szereg klas, ułatwiających tworzenie aplikacji. Wraz z nową edycją KDE 2 współdziałanie pomiędzy aplikacjami i ich powtórne wykorzystanie stanie się łatwiejsze, co stworzy jeszcze potężniejszy i bardziej zintegrowany pulpit.
W tym rozdziale omówimy następujące zagadnienia:
Instalacja Qt i KDE.
Użycie tmake ułatwiające tworzenie i zarządzanie plikiem Makefile.
Utworzenie prostej aplikacji Qt.
Koncepcja sygnału i szczeliny.
Tworzenie widżetów.
Rozmieszczenie widżetów.
Klasy użytkowe Qt.
Wstęp do programowania KDE: prosty edytor tekstowy.
Instalacja Qt
Kod źródłowy dla Qt może być pobrany pod adresem internetowym: ftp://ftp.trolltech.com/qt/source/. Pakiety do zainstalowania, na przykład w formacie RPM, odnaleźć można pod adresem ftp://ftp.trolltech.com/qt/dist/, lub w witrynie internetowej wersji dystrybucyjnej używanego Linuksa. Tutaj omówiona zostanie instalacja Qt z upakowanego archiwum (ang. tarball archive) z kodem źródłowym. W tymże archiwum znajduje się plik INSTALL, opisujący w każdym, najdrobniejszym szczególe sposób instalacji Qt. A podstawowa procedura instalacyjna, skuteczna dla większości systemów, przedstawia się następująco.
Zalecanym katalogiem do umieszczenia Qt jest /usr/local, ale kwestia lokalizacji jest sprawą najzupełniej dowolną. Przykładowo, Qt w dystrybucjach SuSE może być odnalezione w /usr/lib/qt. Tutaj zakładamy lokalizację /usr/local:
$ cd /usr/local
Należy najpierw rozpakować archiwum, używając metody podobnej do poniższej:
$ tar -xvzf qt-x11-2.1.1.tar.gz
Utworzenie dowiązania (ang. link) do katalogu Qt umożliwi instalację wielu wersji Qt:
$ln -s /usr/local/qt-2.2.1.1 /usr/local/qt
Qt potrzebuje dwóch zmiennych środowiskowych:
QTDIR=/usr/local/qt
PATH=$QTDIR/bin:$PATH
Uaktualnienie ścieżki jest niezbędne do poinformowania powłoki systemowej o lokalizacji kompilatora Qt metaobiektów (o którym nieco później). Zmienne środowiskowe należy umieścić w pliku konfiguracyjnym używanej powłoki systemowej, na przykład dla powłoki bash w pliku .bash_profile, który znajduje się w katalogu macierzystym. Plik INSTALL zawiera wyczerpujące wyjaśnienia dotyczące innych powłok. Jeśli zmienna QTDIR zostanie tylko umieszczona w pliku konfiguracyjnym powłoki systemowej, to należy ponownie uruchomić powłokę (to znaczy wyrejestrować się i ponownie zarejestrować w systemie). Można też wykonać następujące polecenie:
$ export QTDIR=usr/local/qt
w celu ustawienia zmiennej w bieżącym środowisku powłoki. Na tym etapie Qt jest przygotowany do skompilowanie. Obejrzenie listy opcji kompilacji umożliwi polecenie:
$ ./configure -help
W domyślnych ustawieniach dla kompilacji brak jest obsługi JPEG i GIF. Aby dołączyć ich obsługę należy wpisać:
$ ./configure -system-jpeg -gif
Jeśli odpowiadają nam ustawienia domyślne, które można zobaczyć używając parametru -help, wystarczy wpisać:
$ ./configure
Może się wyjątkowo pojawić potrzeba określenia platformy, dla której dokonuje się kompilacji. Robi się to tak:
$ .configure -platform linux-g++
To spowoduje utworzenie plików makefile dla biblioteki i wszystkich przykładów. Wystarczy wpisać:
$ make
by skompilować wszystko. Nie ma make install. Teraz należy dodać następujący wiersz do /etc/ld.so.conf:
/usr/local/qt/lib
Zarejestruj się w systemie jako użytkownik root i uruchom:
# ldconfig
I to wszystko. Jeśli pojawią się problemy, pomocny może się okazać plik INSTALL lub lista FAQ (często zadawanych pytań i odpowiedzi) na temat instalacji. W materiałach źródłowych przy końcu tego rozdziału znaleźć można adresy odpowiednich stron WWW.
Instalacja KDE
Będziemy korzystać z najnowszej wersji KDE, czyli z KDE 2. KDE jest bardzo duży, a więc pobranie go nie dla każdego użytkownika może okazać się praktyczne. Zamiast pobierać go w całości zalecana jest jego instalacja jako części dystrybucji Linuksa. Nie powinno to stanowić problemu, gdyż KDE dołączany jest niemal do wszystkich dystrybucji Linuksa. Jeśli jednak zajdzie potrzeba pobrania KDE należy dostosować się do aktualnych instrukcji instalacji podanych w witrynie WWW http://www.kde.org/.
Biblioteki
Qt posiada różnorodne rozszerzenia, a KDE liczne biblioteki. Tutaj skoncentrujemy się na czterech bibliotekach centralnych. Zapoznanie się z nimi przyda się i znacznie ułatwi poszerzanie aplikacji o nowe, dodatkowe biblioteki.
Biblioteka |
Opis |
libqt |
Qt składa się z jednej biblioteki libqt. Istnieja rozszerzenia Qt w oddzielnych bibliotekach, ale je pominiemy. |
libkdecore |
Wszystkie programy KDE używają tej biblioteki. Zapewnia ona podstawowy zestaw funkcji, jak np. system konfiguracji i umiędzynarodowienie. |
libkdeui |
Ta biblioteka zawiera większość elementów interfejsu użytkownika, dostarczanych przez KDE. |
libkfile |
Ta biblioteka dostarcza wielu klas do operacji na plikach, takich jak dialogi otwierania i zapisywania plików, podglądu plików (np. by pokazać podgląd pliku w postaci miniaturowego obrazka) w dialogu otwierania pliku. |
Programowanie aplikacji przy użyciu Qt
Programiści mają do dyspozycji kilka narzędzi do skonstruowania interfejsu graficznego GUI za pomocą KDE/Qt. Jest nim choćby Qt Architect, albo QtEZ, czy też KDE Studio (wszystkie łącza do informacji na ich temat są podane w podrozdziale z materiałami źródłowymi). Nasz pierwszy interfejs GUI będzie napisany samodzielnie, bez ich pomocy. Są ku temu dwa powody. Po pierwsze, interfejs GUI Qt jest bardzo łatwy do odręcznego napisania. Po drugie, to nie wyręczanie się generatorem GUI (ang. GUI builder), ale bezpośrednie użycie Qt jest najlepszą metodą nauki programowania z jego użyciem. Jest rzeczą przydatną wiedzieć, co właściwie dzieje się podczas konstrukcji GUI, aby można było, gdy narzędzia nie mogą sprostać zadaniu, rozszerzyć swój kod samodzielnie.
Czytelnik powinien, do czego zachęcamy, sięgnąć po podręcznik Qt, by dokładnie zapoznać się z możliwościami klas, które tutaj zostaną użyte.
Na początek: Witaj świecie
Zaczniemy od omówienia pojęciowego Qt i utworzymy graficzny program „Witaj świecie”. Następnie rozszerzymy go nieco, zanim przyjrzymy się uważniej Qt. Nasz program będzie składał się z pojedynczego przycisku (ang. push button) z przewidywalnym opisem. Zatem do dzieła!
#include <qapplication.h>
#include <qpushbutton.h>
int main(int argc, char **argv)
{
QApplication app(argc, argv);
QPushButton button("Witaj swiecie", 0);
app.setMainWidget (&button);
button.show();
return app.exec();
}
I to by było na tyle! Trudno o coś krótszego niż to. Teraz kompilujemy i uruchamiamy:
$ g++ -c -I$QTDIR/include main.cpp
$ g++ -o witajswiecie main.o -L$QTDIR/lib -lqt
$ ./witajswiecie
Obiekt QApplication rozpoznaje kilka opcji. By zapoznać się z pełną listą możliwych opcji należy sięgnąć po właściwą dokumentację dla klasy QApplication . Tutaj przedstawimy tylko jeden argument -style. Spróbuj jeszcze uruchomić witajswiecie, określając obsługiwany przez Qt styl (Motif jest ustawiony domyślnie) — jeden z dwóch pozostałych do wyboru: Windows i Platinum:
$ ./witajswiecie -style=platinum
i
$ ./witajswiecie -style=windows
Nawiasem mówiąc, Platinum jest nazwą używaną przez Apple do opisu wyglądu i działania systemu operacyjnego MacOS. Poniżej znajdują się zrzuty ekranu dla trzech różnych stylów. Przycisk jest pojedynczy, toteż różnice w wyglądzie przycisku w zależności od zastosowanego stylu, są bardzo nieznaczne. Najlepiej są one widoczne w kształcie i stylu wyprofilowanych ukośnie krawędzi (ang. bevel) wokół przycisku. Argument style może być dodany do każdego programu Qt. Jeśli aplikacja jednoznacznie nie wymusza wyglądu, ten argument zmieni styl całej aplikacji.
Styl |
Wynik |
Motif (domyślnie) |
Zrzut przycisku I (str.454)
|
Platinum |
Zrzut przycisku II (str.454)
|
Window |
Zrzut przycisku III (str.454)
|
Przyjrzyjmy się uważniej kodowi:
#include <qapplication.h>
#include <qpushbutton.h>
Najpierw dołączamy niezbędne pliki. Zamierzamy użyć tylko dwóch klas QApplication i QPushButton, a więc to wszystko czego nam trzeba.
QApplication app(argc, argv);
Argumenty wiersza poleceń są przekazywane do konstruktora QApplication, by umożliwić mu rozpoznanie, na przykład, argumentu -style. Klasa QApplication zajmuje się rozpoczęciem i zakończeniem aplikacji, strumieniem sterowania (ang. control flow) oraz głównymi ustawieniami aplikacji. Qt dostarcza wielu klas użytkowych, jak również rozszerzenia sieciowego (ang. networking extension). Jest więc możliwe opracowanie z pomocą Qt aplikacji innego typu niż GUI. W takim przypadku nie jest potrzebny obiekt QApplication. Choć zazwyczaj obiekt QApplication występuje, to jednak nie jest to klasa zbyt często wykorzystywana poza przypadkami jej użycia na samym początkiem aplikacji, jak to pokazano powyżej. Zazwyczaj kilka powyższych wierszy jest wszystkim, co należy wiedzieć o QApplication.
QPushButton button("Witaj swiecie", 0);
W ten sposób tworzy się przycisk. Pierwszy argument jest opisem (ang. caption) przycisku, a drugi argument jest tak zwanym obiektem macierzystym (ang. parent) przycisku. Do kwestii „rodzicielstwa” jeszcze powrócimy. Na razie postarajmy zadowolić się stwierdzeniem, że wszystkie widżety w Qt mają widżet macierzysty. Wyjątkiem są okna najwyższego poziomu (ang. top-level windows), które zgodnie z konwencją mają okno główne (ang. root window) jako swój widżet macierzysty. Okna najwyższego poziomu są tworzone poprzez przekazanie wskaźnika pustego (ang null pointer) jako obiekt macierzysty.
app.setMainWidget(&button);
Określamy dla obiektu aplikacji główny widżet tej aplikacji. Główny widżet pełni szczególną rolę — jeśli jest zamknięty, wówczas aplikacja zakończy się.
button.show();
Jak dotąd, przycisk jedynie został utworzony, ale nie pokazany. Powyższy wiersz pozwoli na ukazanie się przycisku. Przycisk pojawi się w swoim własnym oknie, ponieważ przekazaliśmy mu wskaźnik pusty jako widżet macierzysty (nie mieliśmy wyboru, bo nie było innych widżetów).
return app.exec();
W ten sposób sterowanie zostało powierzone Qt, który teraz zadba o przebieg programu: będzie oczekiwał na różne zdarzenia i wykonywał odpowiedni kod. By trochę wzbogacić program powinniśmy wprowadzić do niego sygnały i szczeliny (ang. signals and slots). Zanim to jednak zrobimy, zapoznajmy się z narzędziem ułatwiającym kompilację — z tmake.
Uproszczenie zarządzania plikiem Makefile za pomocą tmake
tmake jest skromnym, ale użytecznym narzędziem ułatwiającym tworzenie i gospodarowanie plikami makefile. Programiści, którzy opracowali Qt, stworzyli narzędzie tmake uznając je za udogodnienie z powodu wielkiej różnorodności plików makefile, potrzebnych do wszechstronnej obsługi platform systemowych. Co więcej, kod źródłowy Qt, używający systemu sygnałów i szczelin, o którym już za chwilę, musi być przetworzony przez, dołączony do Qt, kompilator metaobiektów (ang. meta object compiler) zanim będzie można przekompilować kod źródłowy kompilatorem C++. I tym zajmie się za nas tmake. W konsekwencji, tmake jest użyteczny dla większej rzeszy użytkowników Qt, niż tylko dla samych jego programistów. My również będziemy się nim posługiwać.
Zadaniem tmake jest pobranie pliku projektowego (ang. project file) jako danych wejściowych i utworzenie z nich pliku makefile. Plik projektowy (zgodnie z przyjętą konwencją nazwie pliku projektowego nadaje się rozszerzenie .pro) jest zasadniczo wykazem wszystkich plików źródłowych i nagłówkowych danego projektu:
TARGET = runme
SOURCES = mainwindow.cpp main.cpp
HEADERS = mainwindow.h
Jeśli okaże się, że tmake nie został dołączony do używanej dystrybucji, to można go pobrać z ftp://ftp.trolltech.com/freebies/tmake/.
Uruchamiamy plik projektowy za pomocą tmake w następujący sposób:
$ tmake test.pro -o Makefile
Proste wpisanie make powinno teraz wstępnie przetworzyć kod źródłowy przy użyciu kompilatora metaobiektów i skompilować kod tak, by powstał wykonywalny runme.
Z takim narzędziem uporanie się z sygnałami i szczelinami będzie o wiele łatwiejsze. Nie potrzeba się będzie już martwić o kompilator metaobiektów moc.
Sygnały i szczeliny
Sygnały i szczeliny (ang. signals and slots) są sposobem Qt na obsługę programowania sprowokowanego wydarzeniami (ang. event-driven programming). Najczęściej sygnały i szczeliny służą do połączenia zdarzeń w interfejsie użytkownika, wywołanych przez użytkownika, z kodem w aplikacji, odpowiadającym na owe zdarzenia. Przykładowo, kiedy użytkownik wybiera jakiś element menu, chcemy podjąć stosowną akcję. Skoro interfejsy użytkownika przepełnione są tego rodzaju logiką, byłoby wygodnie, aby istniał sposób uproszczenia tego kodu. I tu przychodzą z pomocą sygnały i szczeliny. Co więc zrobi Qt, gdy użytkownik wybierze jakiś element menu? Qt wyemituje sygnał (ang. signal). Możemy wysłuchać owego sygnału, a dzieje się to poprzez przyłączenie do niego szczeliny (ang. slot). Jeśli to „przyłączenie” przypomina Czytelnikowi funkcję wywołania zwrotnego (ang. callback) nie jest On w błędzie. Sygnały i szczeliny są najzwyczajniej funkcjami wywołania zwrotnego. Są jednak bezpieczne typologicznie (ang. type-safe) i łatwiejsze w użyciu, gdyż skrywają przed użytkownikiem wiele bolesnych szczegółów.
System sygnał i szczelina jest niezwykle plastyczny. Obiekt Qt może wysyłać dowolną liczbę sygnałów, a także obiekt Qt może wysłuchać dowolnej liczby sygnałów. Sygnał może mieć dowolna liczbę przyłączonych doń szczelin i vice versa. Możliwe jest nawet przyłączenie sygnałów do sygnałów (pierwszy sygnał uwalnia drugi sygnał). W praktyce jednak jest to rzadko stosowane. Sygnały mogą przyjmować dowolną ilość argumentów każdego typu i są bezpieczne typologicznie.
Istnieją jednak pewne ograniczenia, choć mają one charakter bardziej akademicki. Dwie najważniejsze restrykcje do napotkania z największym prawdopodobieństwem to:
sygnały nie mogą być używane w szablonach,
klasa QObject musi być pierwszą klasą na liście klas dziedziczenia wielokrotnego (ang. multiple inheritance).
Pierwsze ograniczenie nie powinno nastręczać żadnych trudności, bo sygnały i szczeliny są zwykle używane razem z widżetami, a klasa widżetów szablonu (ang. template widget class) nie zdarza się zbyt często. Druga kwestia nie jest właściwą restrykcją, ale czymś co trzeba zapamiętać.
Sygnały i szczeliny będą zatem dla nas ważnym narzędziem ożywienia tworzonych przez nas interfejsów. Jak wspomniano, sygnał należy połączyć ze szczeliną. Odbywa to się w następujący sposób:
connect( signal_emitter, SIGNAL(a_signal())'
slot_owner, SLOT(a_slot()));
Funkcja connect znajduje się w klasie QObject. Zakładamy więc, że powyższy wiersz jest wywołany z klasy pochodzącej od QObject. Oznacza to, że wszystkie klasy zawierające sygnały i (lub) szczeliny muszą pochodzić od QObject. Może to zabrzmieć jak nudna restrykcja, jednakże sygnały i szczeliny są najczęściej używane w kodzie GUI. Wymóg ten rzadko stanowi problem praktyczny, gdyż widżet QWidget, który jest klasą podstawową (ang. base class) wszystkich widżetów, pochodzi od QObject.
Oto przykład funkcji connect. O budowie innych, potrzebnych jej konstrukcji powiemy już niebawem.
connect( button, SIGNAL(clicked())),
this, SLOT(shutdown()));
Pierwszy argument, zazwyczaj widżet, jest nadawcą sygnału. Drugi argument jest tym, do czego sygnał nadawcy jest podłączany, w tym przykładzie jest to clicked („kliknięty”). Trzeci argument to odbiorca sygnału, zwykle również widżet. Na koniec przekazaliśmy szczelinę. W tym przypadku jest nią funkcja shutdown. Szczelina jest naturalnie usytuowana w obiekcie odbiorcy. Zamieniamy wspomniane funkcje w sygnały i szczeliny za pomocą makrodefinicji (ang. macros) SIGNAL i SLOT, o czy będzie jeszcze mowa później.
Chociaż shutdown jest funkcją regularną i tak może być traktowana, to należy zdefiniować ją jako szczelinę. Jest to zrobione w pliku nagłówkowym. Przyjrzyjmy się plikowi nagłówkowemu mywidget.h, który posiada wszystko, co jest wymagane do pracy z sygnałami i szczelinami:
class MyWidget : public QWidget
{
Q_OBJECT
public:
MyWidget();
virtual ~MyWidget();
signals:
void mysignal();
Taka niestandardowa etykieta jest, być może, nieco niepokojąca. Jak się jednak okaże, klasy Qt, używające sygnałów i szczelin będą przetwarzane wstępnie przez preprocesor (ang. preprocessor), co zapewni, że powyższy kod będzie strawny dla kompilatora C++. Preprocesor zadba również o taką etykietę:
private slots:
void shutdown();
private:
...
}
Wyprowadzamy klasę pochodną od QWidget, a więc również od klasy QObject (zobacz diagram klas poniżej). Pochodzenie od QObject jest pierwszym wymogiem dla użycia sygnałów i szczelin. Drugi wymóg to konieczność włączenia makrodefinicji Q_OBJECT do deklaracji klasy. Wystarczy tylko spełnić te dwa warunki i klasa jest przygotowana do użycia sygnałów i szczelin. Własne, indywidualnie zdefiniowane sygnały i (lub) szczeliny również muszą być wyspecyfikowane w pliku nagłówkowym. Powyżej zadeklarowano sygnał mysignal oraz już wspomnianą szczelinę shutdown.
Określony został też sygnał w pliku nagłówkowym. Aby wyemitować ten sygnał wystarczy napisać:
emit mysignal():
gdziekolwiek w kodzie dla MyWidget. Należy zauważyć przy tym, że w przeciwieństwie do szczelin sygnały nie mają żadnej implementacji (implementacja jest efektywnie zrealizowana przez szczeliny odbiorcze). Sygnały są tylko deklaracjami funkcji. Szczeliny są funkcjami regularnymi i jako takie są zgłaszane:
void MyWidget::shutdown()
{
...
}
Sygnały i szczeliny mogą również posiadać argumenty. W pliku nagłówkowym są one określone jak każda inna deklaracja funkcji. Szczeliny są implementowane tak, jak inne funkcje z argumentami. Należy zapamiętać, że używając makrodefinicji SIGNAL i SLOT trzeba określić typy, a nie zmienne. Przykładowo, QlistView, klasa do tworzenia drzew (ang. trees) i wielokolumnowych list, definiuje sygnał w następujący sposób:
void clicked(QlistViewItem *, const QPoint &, int);
Połączenie sygnału ze szczeliną w naszym widżecie wymaga takiego wpisu:
connect(listview, SIGNAL(clicked(QlistViewItem*, const Qpoint&, int)),
this, SLOT(our_slot(QlistViewItem*, const QPoint&, int)));
Należy zauważyć, że sygnatury (ang. signatures) sygnału i szczeliny muszą do siebie pasować. Sygnały muszą mieć void jako typ zwracany. Jest to rozsądne, bo sygnał może być odebrany przez dowolną liczbę szczelin, albo przez, zgoła, żadną, co owocuje różnymi wynikami. By uniknąć niepotrzebnego zamieszania, nadawca sygnału nie uzyskuje żadnej informacji zwrotnej (ang. feedback) o wyniku sygnału.
Oczywiście, signals:, private slots:, czy emit są słowami kluczowymi, którymi kompilator C++ mógłby się udławić. Na tym etapie przyjdzie z pomocą kompilator metaobiektów moc (ang. meta object compiler). Kompilator moc jest preprocesorem przeglądającym deklarację klasy w poszukiwaniu makrodefinicji Q_OBJECT. Jeśli ją odnajdzie, wówczas utworzy dodatkowy plik C++, zawierający kod metaobiektu dla tej klasy. Utworzony plik może zostać dołączony do kodu źródłowego, ale zwykle jest kompilowany osobno i konsolidowany z resztą plików wynikowych. Tak działa moc, ale jak już widzieliśmy, tmake przejmie kod, powierzy go kompilatorowi moc, a następnie skompiluje i skonsoliduje. Tak więc w praktyce, wszystko co wystarczy wiedzieć na ten temat, to pamiętać o wyprowadzeniu z QObject (albo jego podklasy) i dołączeniu Q_OBJECT do deklaracji klasy.
Zrozumienie koncepcji sygnałów i szczelin jest wielce istotne. Sygnały i szczeliny są bowiem niezbędne przy pisaniu aplikacji Qt.
Jeżeli system SIGNAL-SLOT jest użyty nieprawidłowo, pojawić się może komunikat o błędzie o takiej treści:
myobject.o(.text+0x390): undefined reference to ′MyObject virtual table′
Zdarza się to zwykle, gdy instrukcja Q_OBJECT nie została włączona do pliku nagłówkowego i (lub) kompilator moc nie był użyty na plikach. Zastosowanie tmake powinno pozwolić na ominięcie takich problemów.
„Witaj świecie” ponownie
Zmienimy kod w ten sposób, żeby aplikacja kończyła działanie w momencie kliknięcia przycisku. Do osiągnięcia tego celu wystarczy dodanie jednego wiersza kodu:
#include <qapplication.h>
#include <qpushbutton.h>
int main(int argc, char **argv)
{
QApplication app(argc, argv);
QPushButton button("Witaj swiecie", 0);
QObject::connect(&button, SIGNAL(clicked()), &app, SLOT(quit()));
app.setMainWidget(&button);
button.show();
return app.exec();
}
Sygnał (clicked, zdefiniowany w klasie QButton, która jest klasą podstawową dla klasy QPushButton) jest połączony ze szczeliną (quit, zdefiniowaną w QApplication). Aplikacja jest skompilowana i uruchomiona jak uprzednio. Teraz, kiedy kliknięty zostanie przycisk, widżet przycisku (ang. button widget) emituje sygnał clicked (kliknięto), który powoduje wykonanie się szczeliny aplikacji quit (zakończ).
Pochodzenie od klas podstawowych
Przy konstrukcji prawdziwej aplikacji korzysta się z widżetów w sposób inny niż tu do tej pory demonstrowany. Ogólną zasadą tworzenia interfejsu GUI jest wyprowadzenie z odpowiedniej klasy podstawowej i zapełnienie konstruktora nowej klasy kodem tworzącym i rozmieszczającym widżety. Na przykład, utworzenie okna dialogowego (ang. diolog box) wiąże się z wyprowadzeniem klasy pochodnej z, załóżmy, QDialog i zaprojektowanie GUI w nowym konstruktorze.
Qt dostarcza wiele takich klas podstawowych do tworzenia okien (ang. windows), czy okien dialogowych (ang. dialog boxes). Do wyprowadzenia może posłużyć podstawowy QWidget, albo, jeszcze częściej, QDialog. Do tworzenia dialogów ze stronami użyć można klasy QTabDialog. Tutaj przedstawimy QMainWindow, klasę, którą można wykorzystać, o ile nie we wszystkich, to w większości aplikacji. QMainWindow zapewnia łatwe tworzenie paska menu (ang. menu bar), pasków narzędzi (ang. tool bar) i paska stanu (ang. status bar).
Lepsze omówienie projektu Qt dostarczy poniższa uproszczona i niekompletna hierarchia dziedziczenia:
Zobaczmy, jak wykorzystać QMainWindow jako klasę podstawową. Zaczniemy od pliku nagłówkowego mainwindow.h:
#include "qmainwindow.h"
class MainWindow : public QMainWindow
{
public:
MainWindow(QWidget *parent =0, const char *name = 0);
virtual ~MainWindow();
};
I to jest wszystko, co jest potrzebne do utworzenia okna głównego. W gruncie rzeczy, można by nawet było opuścić argumenty konstruktora, bo żadne z nich nie będą nam potrzebne. Należy jednak przywyknąć do tworzenia tych dwóch argumentów we wszystkich tworzonych od nowa widżetach. Są to dwa standardowe argumenty, występujące we wszystkich widżetach — nie chcemy tutaj zaburzać spójności, pomijając je. Jak widać, obu argumentom nadano domyślne wartości tak, aby można było je zignorować podczas faktycznego użycia klasy. Najpierw jest widżet macierzysty (ang. parent widget), a potem nazwa tego widżetu. W przykładzie „Witaj świecie” przekazany został , jako pierwszy argument, łańcuch opisu (ang. caption) przycisku, a jako drugi argument widżet macierzysty. To jednak nie było nazwą widżetu. Nazwa jest użyta tylko wewnętrznie i nigdy nie jest widoczna dla użytkownika. Przyjrzymy się dokładniej obu argumentom nieco później.
Kod źródłowy jest równie prosty:
#include "qmainwindow.h"
MainWindow::MainWindow(
QWidget *parent,
const char *name)
: QMainWIndow(parent, name)
{
}
MainWindow::~MainWindow()
}
}
Teraz mamy już całkowicie prawidłowy widżet, gotowy do użycia. Zaznaczyć tu należy, że widżet ten nie może być użyty do niczego pożytecznego. Jest on tylko szkieletem konstrukcyjnym. Pokazanie tego okna, wymaga zastosowania kodu, który jest niemal identyczny z użytym w „Witaj świecie”. Jest tak, bo zarówno przycisk, jak i nasze okno główne pochodzą od QWidget i używamy interfejsu QWidget w main:
include <qapplication.h>
#include "mainwindow.h"
int main(int argc, char **argv)
{
QApplication app(argc, argv);
MainWindow m;
app.setMainWidget(&m);
m.show();
return app.exec();
}
Tworzymy okno główne, ustanawiamy go głównym widżetem i pokazujemy go. Wywołanie exec wchodzi do głównej pętli zdarzeń (ang. main event loop) i pozostanie tam do momentu zamknięcia okna głównego, dokładnie tak jak w przykładzie „Witaj świecie”.
Tworzymy plik projektowy o nazwie mainwindow.pro:
TARGET = mainwindow
SOURCE = main.cpp mainwindow.cpp
HEADERS = mainwindow.h
Plik projektowy jest przesłany do tmake, który tworzy plik makefile. Nasz kod może być więc skompilowany i uruchomiony:
$ tmake mainwindow.pro -o Makefile
$ make
$ ./mainwindow
I otrzymujemy:
Zrzut rysunku str. 461
|
Wyprowadzenie klasy pochodnej z QMainWindow, tak jak to przedstawiono, jest zakodowaniem specjalizacji. Zasada ta odnosi się do każdego innego widżetu Qt (czy KDE). Jeśli, dla przykładu, nie odpowiada nam standardowe działanie QPushButton, to możemy zwyczajnie użyć tej klasy do wyprowadzenia widżetu, który dostosujemy do naszych indywidualnych potrzeb.
Nie będziemy na razie zajmować się naszym oknem i przejdziemy do szczegółowej analizy dwóch najważniejszych, obok sygnałów i szczelin, zagadnień programowania Qt. Mianowicie, widżetów (ang. widgets) i układów (ang. layouts).
Widżety
Każdy widoczny element w interfejsie graficznym użytkownika (GUI) Qt, interakcyjny, czy nie, jest widżetem. QWidget jest klasą podstawową wszystkich widżetów. Wszystkie widżety są wzajemnie połączone związkami hierarchicznymi (rodzinnymi) typu przodek - potomek (parent-child retationship). Odbywa się to przez przekazanie widżetu macierzystego do konstruktora widżetu potomnego przy tworzeniu nowych widżetów. Już o tym krótko wspomnieliśmy wcześniej. Na przykład, gdybyśmy chcieli utworzyć nowy przycisk w naszym oknie głównym, widocznym powyżej, mniej więcej tak wyglądałby kod:
MainWindow::MainWindow(QWidget *parent, const char *name)
: QMainWindow(parent, name)
{
QPushButton *quit_button = new QPushButton("&Quit", this, "quitbutton");
...
}
Znak „&” poprzedzający „Q” w wyrazie „Quit” informuje Qt o klawiszu skrótu (ang. shortcut key) dla tego przycisku i o tym, by podkreślić literę Q. Najważniejsze tu jest jednak utworzenie przycisku, który jest potomkiem okna głównego. Przekazanie konstruktorom 'this' jest bardzo często spotykane w Qt. Przekazanie wskaźnika pustego jako przodka uczyni z widżetu nowe okno najwyższego poziomu. Tak więc, jeśli przekażemy zero do konstruktora przycisku powyżej, otrzymamy małe okno z pojedynczym przyciskiem, oddzielonym od okna dialogowego. Zero jest wartością domyślną dla konstruktorów widżetów.
Drugim argumentem dla wszystkich widżetów jest obowiązkowa nazwa widżetu. W Qt można nadać wszystkim widżetom nazwę, która jest używana wyłącznie wewnętrznie. Nie należy mylić nazwy widżetu z tekstowymi etykietami widocznymi dla użytkownika. W wierszu powyżej „Quit” jest opisem (ang. caption) przycisku, ale nie jego nazwą. Nazwą jest quitbutton. Moglibyśmy ją pominąć, określając przycisk w ten sposób:
QPushButton *quit_button = new QPushButton("&Quit", this);
Tej formy użyto w pierwszym programie „Witaj świecie”. Nazwa przydaje się najczęściej przy usuwaniu błędów (ang. debugging). Na przykład, Qt może drukować ostrzeżenia i jeśli będą one należeć do jakiegoś widżetu, Qt wydrukuje jego nazwę, przez co łatwo uda się wyśledzić nieprawidłowo użyty widżet. Inne narzędzie do usuwania błędów zrzuca wszystkie obiekty QObject znajdujące się na drzewie związków rodzinnych (przodek-potomek). Nazwy te mogą być także wykorzystane później do wyszukania widżetu, ale jest mało prawdopodobne, iż kiedykolwiek będzie to potrzebne. Dla wygody nie będziemy tu nazywać naszych widżetów, ale nazywanie widżetów powinno stać się pożytecznym przyzwyczajeniem.
Wszystkie widżety w Qt, mające przodka (t.j. nie będące oknami najwyższego poziomu) MUSZĄ być utworzone przy pomocy polecenia „new”. Qt zapewnia automatyczną dealokację tych widżetów.
Jeśli nie ma potrzeby dostępu do utworzonych widżetów dla np. odczytu wartości, ich uaktywnienia, czy też wyłączenia, to nie trzeba też przechowywać wskaźników dla dealokacji. Stąd też, destruktory często pozostają puste.
Poniżej znajduje się zrzut ekranu przykładowej aplikacji widżetów, widgets, dołączonej do dystrybucji Qt. Okno nie pokazuje wprawdzie wszystkich widżetów, jakie Qt oferuje, ale jest ich na tyle, by pojąć, jak wiele można osiągnąć za pomocą Qt. Jak widać, wykorzystano wygląd i styl charakterystyczny dla Windows:
Rysunek str. 462
|
Układy
Teraz, gdy potrafimy już tworzyć widżety, potrzebny nam będzie nadzór nad ich rozmieszczeniem. Do tego najprzydatniejsze będzie użycie klas układu (ang. layout classes). Układ (ang. layout) nie jest widżetem, chociaż Qt udostępnia kilka widżetów, które mogą zostać wykorzystane do rozmieszczenia innych widżetów. Należy o tym pamiętać, ponieważ widżetami macierzystymi dla widżetów mogą być tylko widżety, a nie układy. W izolowanych przypadkach może się zdarzyć niezbędne użycie jednego z widżetów układowych Qt (ang. Qt's layout-widgets) do rozmieszczenia. W każdym bądź razie, wydawać się może niezręczne ich użycie, jeśli użytkownik przywykł do rozmieszczania widżetów, posługując się ustalonym układem współrzędnych. Dzięki układom można tworzyć automatycznie schludnie dopasowane, rozróżniające wielkość czcionek i zdolne do zmiany rozmiarów interfejsy GUI.
Qt zapewnia pewną liczbę klas układu. Można również utworzyć własną klasę, jeśli ma się jakieś specjalne wymagania dotyczące układu widżetów, jak choćby, ułożenie widżetów w krąg. Zwykle jednak wystarczają istniejące klasy układu. Tutaj używać będziemy głównie układów QVBoxLayout i QHBoxLayout do rozmieszczenia, odpowiednio, pionowego i poziomego.
Układy zawierają widżety i (lub) inne układy. Przykładowo, chcemy takiego układu:
Rys. 1, str. 463
|
Widżet listy i widżet przycisku są ułożone tak (pasek przewijania jest częścią listy, więc nie martwimy się o niego):
Zanim rozpoczniemy pisanie kodu, spróbujmy zreasumować nasze plany. Jeśli myślowo zgrupujemy elementy tego okna dialogowego, to otrzymamy pole listy (ang. list box) i umieszczone poniżej dwa przyciski poziomo ułożone. Inaczej mówiąc, na najbardziej zewnętrznym poziomie należy umieścić dwie grupy jedna nad drugą. Najbardziej naturalnym sposobem jest użycie układu pionowego (ang. vertical layout). Do tego celu służy dostarczony przez Qt QVBoxLayout. Zbudujmy hierarchię obiektów tego układu. Niewielka praktyka umożliwi każdemu zbudowanie podobnych hierarchii całkiem naturalnie. Tak więc mamy układ pionowy i dwie grupy:
QVBoxLayout
Group1
Group2
Graficznie wygląda to tak:
Pierwsza grupa jest po prostu polem listy i dlatego nie jest właściwie grupą czegokolwiek. Przyciski, jednakże, powinny być zgrupowane poziomo. Jeżeli dwa przyciski będą wstawione w układzie pionowym, wówczas otrzymamy dwa przyciski jeden nad drugim:
Rysunek, str. 464
|
Nie jest to oczekiwany układ. Potrzebny więc będzie układ poziomy, albo, mówiąc precyzyjniej, QHBoxLayout. Wewnątrz tego układu umieszczamy dwa przyciski. Ostateczna hierarchia wygląda następująco:
QVBoxLayout
QListBox
QHBoxLayout
QPushButton
QPushButton
Jeśli uważniej przyjrzymy się pierwszemu zrzutowi ekranu, zauważymy po lewej stronie dwóch przycisków pusty obszar. Zmiana rozmiarów okna dialogowego utrzyma bez zmian wymiary przycisków przy powiększeniu się wymiaru pustego obszaru. Można tego dokonać poprzez wstawienie tak zwanego rozciągacza (ang. stretch) po lewej stronie przycisków.
Po rozpracowaniu logiki i architektury okna dialogowego, pora przyjrzeć się kodowi:
#include "dialog.h"
#include <qlayout.h>
#include <qlistbox.h>
#include <qpushbutton.h>
Dialog::Dialog(QWidget *parent, const char *name) : QDialog(parent, name)
{
Tutaj wyprowadzamy pochodną z QDialog, ale równie dobrze można wykorzystać do tego celu QWidget jako klasę podstawową. QDialog jest wyprowadzony z QWidget i oferuje między innymi modalność (ang. modality) oraz przyciski domyślne (ang. default buttons).
QVBoxLayout *vertical = new QVBoxLayout(this);
QHBoxLayout *horizontal = new QHBoxLayout;
Mamy tutaj dwa układy w oknie dialogowym. Należy zauważyć, że tylko jeden z nich ma okno dialogowe jako obiekt macierzysty. Jeśli więcej niż jeden układ miałby ten sam, szczególny widżet macierzysty, to wówczas pojawiłoby się ostrzeżenie w trakcie wykonywania programu. Układ poziomy stanie się później potomkiem układu pionowego.
QlistBox *listbox = new QlistBox(this):
for(int i = 1; i <= 10; i++)
{
QString str = QString("Element number %1").arg(i);
QString jest jedną z klas zapewnianych przez Qt, która nie jest klasą typu GUI. QString jest potężną klasą i Qt wykorzystuje ją szeroko. Klasa łańcucha używa zliczeń odsyłaczy (ang. reference counting), a ponieważ Qt prawie wszędzie z tego korzysta, jest ona dlatego bardzo wydajna. Używanie innej klasy łańcucha, takiej jak na przykład klasy łańcucha STL, wraz z Qt spowodowałoby niepotrzebne powielanie. Co więcej, Unicode jest używany tam wewnętrznie, udostępniając obsługę każdego języka. Tak buduje się łańcuch ze zmienną:
listbox->insertItem(str);
}
To tworzy pole listy i jej zawartość:
QPushButton *okay = new QPushButton("Okay", this);
QPushButton *cancel = new QPushButton("Cancel", this);
czyli nasze dwa przyciski. Należy zwrócić uwagę na to, że zarówno pole listy jak i przyciski mają okno dialogowe jako widżet macierzysty, a nie układy w jakich były umieszczone. Jest tak dlatego, bo widżetami macierzystymi widżetów mogą być tylko widżety. Układy, jak już o tym wspomniano, nie są widżetami. Teraz, mając utworzone widżety, można przystąpić do ich organizacji w pożądanym układzie:
horizontal->addStretch();
horizontal->addWidget(okay);
horizontal->addWidget(cancel);
I tak został utworzony poziomy układ. Widżety, odstępy i rozciągacze są dodawane w porządku od lewej do prawej. Dodanie rozciągacza pozwala na utrzymanie stałych poziomych rozmiarów przycisków (ich rozmiary pionowe już zostały ustalone przez implementację QPushButton).
vertical->addWidget(listbox);
vertical->addLayout(horizontal);
W ten sposób tworzy się układ pionowy z polem listy u góry i poziomym układem u dołu (porządek od góry do dołu). Należy zwrócić uwagę na użycie innej funkcji dla dodania układu.
resize(200, 150);
}
Na koniec został ustalony rozmiar okna dialogowego. Do kompletu, poniżej przedstawiona jest główna funkcja otwarcia okna:
#include <qapplication.h>
#include "dialog.h"
int main(int argc, char **argv)
{
QApplication app(argc, argv);
Dialog dlg;
app.setMainWidget(&dlg);
dlg.show():
return app.exec();
}
Tworzymy plik layout.pro:
TARGET = layout
SOURCES = main.cpp dialog.cpp
HEADERS = dialog.h
Przetworzenie pliku projektowego za pomocą tmake, kompilacja i uruchomienie wygląda tak:
tmake layout.pro -o Makefile
make
./layout
Jak wspomniano na początku tego podrozdziału, niekiedy trzeba skorzystać z tego, co niedbale nazwaliśmy „widżetami układowymi Qt” (ang. „layout-widgets”). Konkretniej, takimi specjalnymi widżetami są QGrid, QHBox, QVBox, QGroupBox oraz ich pochodne. Nie będziemy się tutaj zajmować wszystkimi widżetami układowymi, omówimy jedynie dla przykładu widżet QButtonGroup, pochodzący od QGroupBox.
QGroupBox zapewnia wyprofilowane ukośnie krawędzie (ang. a bevel) wokół zawartości oraz opis. Przydaje się to przy grupowaniu spokrewnionych widżetów. Podobny do QGroupBox, widżet QButtonGroup specjalizuje się w widżetach QButton i posiada szereg właściwości, umożliwiających mu zajmowanie się nimi. Na przykład, QButton może być ustawiony na „wyłączność” (ang. 'exclusive'). Ten tryb zapewnia, że w danej grupie przycisków tylko jeden przycisk w danym momencie będzie przełączony. Sprawdza się to zwłaszcza w przypadku przycisków opcji (ang. radio buttons). Poniżej znajduje się przykład pola grupy (ang. group box):
Rys, str. 466
|
Ogólnie mówiąc, klasy układu zapewniają zestaw funkcji dla rozmieszczania widżetów, odstępów, rozciągaczy i podukładów (ang. sub layouts). Wspomniane widżety układowe umożliwiają ustawienia jedynie w ograniczonym zakresie, ale za to oferują możliwości obsługi ich zawartości lub wyprofilowania ukośnych krawędzi.
Pionowe i poziome układy, choć są tylko prostymi blokami składowymi, dzięki łączeniu ich na różne sposoby, mogą posłużyć do stworzenia prawie dowolnego GUI. Niekiedy istnieje potrzeba rozmieszczenia widżetów na wzór siatki. I choć można użyć w tym celu układów poziomych i pionowych, to jednak wygodniej jest wykorzystać QGridLayout.
I na tym kończymy omawianie Qt. Jego możliwości są jednak znacznie rozleglejsze, niż zdołaliśmy tu przedstawić. Czytelnika zachęcamy do zapoznania się z takimi właściwościami Qt jak przeciągnij i upuść (ang. drag and drop), zarządzanie sesją (ang. session managment) oraz umiędzynarodowienie (ang. internalization). Nabyte w tym rozdziale podstawy Qt pozwolą wykorzystać pozostałe możliwości tego pakietu narzędziowego oraz przejść do programowania KDE.
Programowanie aplikacji przy użyciu KDE
Tworzenie aplikacji KDE wygląda bardzo podobnie, jak w przypadku aplikacji Qt. KDE zapewnia, oczywiście, wiele własnych klas, jak też rozszerzonych klas Qt, ale obowiązują te same koncepcje widżetów, układów i systemu sygnału i szczeliny SIGNAL-SLOT. Jeden z ważniejszych aspektów pisania aplikacji KDE wiąże się nie tyle ze stroną techniczną procesu tworzenia, ile z kultywowaniem dobrej praktyki posługiwania się poradnikiem stylu. Poradnik ten można znaleźć (nie jest dołączony do pakietów) w Internecie pod adresem: http://developer.kde.org/documentation/standards/kde/style/basics/index.html.
Ten podrozdział nie będzie traktował o projektowaniu GUI. Skupimy się na stronie technicznej i nie pozwolimy, by poradnik stylu zasłonił nam ten punkt widzenia.
Prosty edytor tekstowy
Przykładem aplikacji KDE będzie prosty edytor tekstowy, który tu opracujemy. Posiadać on będzie:
pasek menu z następującymi możliwościami: new (nowy), open (otwórz ), save (zapisz) i zakończ quit (zakończ);
pasek narzędzi z możliwościami: new (nowy), open (otwórz ), save (zapisz) i quit (zakończ);
właściwy edytor.
Zacznijmy od okna głównego — będzie to jedyne okno, utworzone samodzielnie przez nas. Do utworzenia dialogów ładowania i zapisywania wykorzystamy istniejące klasy. Podobnie jak w przykładzie dla Qt, zamierzamy wykorzystać klasę „okno główne” (`main window'). Klasa jest teraz nazwana KTMainWindow. Ogólna idea wyprowadzania z niej klasy pochodnej pozostaje niezmienna. KTMainWindow zapewnia kompleksowe zarządzanie sesją (zapisuje swoją pozycję, geometrię i pozycję pasków narzędzi). Jest zaznajomiony z ustawieniami KDE, co pozwala na automatyczne ustawienia czcionek, kolorów i wzornictwa pulpitu (ang. themes), zgodne z bieżącymi ustawieniami KDE. Plik editorwindow.h wygląda tak:
#include <ktmainwindow.h>
#include <keditcl.h>
class EditorWindow : public KTMainWindow
{
Q_OBJECT
public:
EditorWindow();
W przeciwieństwie do Qt, okno główne KDE nie ma widżetu macierzystego. Odradza się również określanie nazwy dla niego, gdyż wszystkie okna główne w aplikacji KDE muszą mieć niepowtarzalne nazwy dla prawidłowego przebiegu zarządzania sesją. Nieokreślenie nazwy dla okna głównego sprawi, że klasa zapewni mu nadanie unikatowej nazwy. Z tego powodu istnieć będzie tylko jeden konstruktor domyślny:
virtual ~EditorWindow();
private slots:
void newFile();
void openFile();
void saveFile();
private:
KEdit *m_pEditor;
};
Jak można zauważyć, plik nagłówkowy wygląda tak, jak wyglądałby plik nagłówkowy Qt. Jedyna różnica polega na użyciu tu klas KDE. A teraz przechodzimy do implementacji naszego edytora (editorwindow.cpp):
#include "editorwindow.h"
#include <kaction.h>
#include <kapp.h>
#include <kfiledialog.h>
#include <kmenubar.h>
#include <kmessagebox.h>
#include <kstdaccel.h>
#include <qfile.h>
#include <qpopupmenu.h>
#include <qtextstream.h>
EditorWindow::EditorWindow()
{
KDE dostarcza klasy KAction dla wygody tworzenia spokrewnionych elementów menu i przycisków paska narzędzi. Jeśli więc zostałby utworzony taki pasek narzędzi:
Rys.1, str. 468. |
to klasa KAction utworzyłaby elementy menu zgodne z tym paskiem narzędzi. Tak więc menu wyglądałoby jakoś tak:
Rys.2, str. 468.
|
KAction steruje centralnie ustawieniami stanów widżetów. Nie ma więc ma ryzyka, że zostanie uaktywniony element menu podczas gdy jemu odpowiadający przycisk na pasku narzędzi jest nieaktywny. Inna cechą klasy KAction jest to, że próbuje umieścić ikonę paska narzędzi zgodnie z bieżącym wzornictwem pulpitu:
KAction *new_action = new KAction(
"&New",
"filenew",
KStdAccel::key(KStdAccel::New),
this,
SLOT(newFile()),
this);
Łańcuch &New jest łańcuchem elementu menu. Znak „&” określa symbol do podkreślenia, wskazując klawisz skrótu Ctrl-N. Łańcuch filenew jest nazwą pliku (z katalogu ikon KDE, rozszerzenie nie jest podane) ikony dla przycisku paska narzędzi. Miniatura ikony umieszczona będzie też przy elemencie menu, jak pokazano powyżej. KDE rozmieści za nas najlepsze ikony, zgodnie z wzornictwem pulpitu i rozmiarem.
KStdAccel oferuje wiele standardowych klawiszy skrótów klawiatury. Element menu New jest jednym z elementów standardowych. Będziemy stosować się do zaleceń poradnika stylu w tej materii. Następnie jest obiekt i szczelina SLOT, które będą połączone zarówno z elementem menu, jak i przyciskiem paska narzędzi. Na zakończenie występuje obiekt macierzysty samego działania.
Dla powszechnych działań, takich jak New, KDE oferuje nam KStdAction, jeszcze łatwiejszą w użyciu:
KAction *new_action = KStdAction::openNew(this, SLOT(newFile()), this);
Dla każdego elementu menu i przycisku paska narzędzi tworzymy podobny obiekt działania (ang. action object):
KAction *open_action = new KAction(
"&Open...",
"fileopen",
KStdAccel::key(KStdAccel::Open),
this,
SLOT(openFile()),
this);
KAction *save_action = new KAction(
"&Save...",
"filesave",
KStdAccel::key(KStdAccel::Save),
this,
SLOT(saveFile()),
this);
KAction *quit_action = new KAction(
"&Quit...",
"stop",
KStdAccel::key(KStdAccel::Quit),
KApplication::kApplication(),
SLOT(quit()),
this);
Teraz utworzymy nasze menu za pomocą QPopupMenu z Qt. Menu rozwijane (ang. popup menu) zostanie umieszczone pod etykietą File (Plik) w pasku menu:
QPopupMenu *file_menu = new QPopupMenu;
menuBar()->insertItem("&File", file_menu);
Jesteśmy teraz przygotowani do zaprojektowania naszego menu poprzez wykorzystanie funkcji plug dla działania. Funkcja ta wstawia działanie do danego widżetu (zwykle do menu lub paska narzędzi). Włączenie menu do działania sprawi, że menu zyska nowy element, wraz z ikoną i klawiszem skrótu. Połączymy to z bezpośrednim odwołaniem do menu, aby wstawić w nim separatory:
new_action_>plug(file_menu);
file_menu->insertSeparator();
open_action->plug(file_menu);
save_action->plug(file_menu);
file_menu->insertSeparator();
quit_action->plug(file_menu);
Pasek narzędzi jest również utworzony za pomocą funkcji plug dla poszczególnych działań:
new_action->plug(toolBar());
open_action->plug(toolBar());
save_action->plug(toolBar());
quit_action->plug(toolBar());
Na zakończenie tworzymy widżet edytora (ang. editor widget) i ustawiamy go jako View (Widok) w oknie głównym. Będzie to po prostu oznaczało, że edytor zajmie większość powierzchni okna głównego:
m_pEditor = new KEdit(this);
setView(m_pEditor);
}
Tak wyglądało projektowanie GUI. Teraz pora na implementację rzeczywistych funkcji. Spróbujemy uczynić edytor przynajmniej odrobinę przyjaznym użytkownikowi. Poza tym, powinniśmy zapewnić, aby aplikacja nie zakończyła się bez zapisu danych, chyba, że użytkownik na to zezwoli. Moglibyśmy wprowadzić do destruktora taki fragment kodu zapisujący dane, bo destruktor jest wzywany w momencie zamykania przez użytkownika okna głównego. Nie jest to chyba najlepsze rozwiązanie — gdyby, na przykład, użytkownik chciał tylko zapisać dane i pozostać w aplikacji, nie byłoby to możliwe. My pozostaniemy przy prostych rozwiązaniach.
Metodą alternatywną byłoby zastąpienie zdarzenia zamknięcia (ang. close event) w taki sposób, aby użytkownik chcąc pozostawić działającą aplikację, mógł odrzucić to zdarzenie. Zagadnienie to wykracza jednak poza ramy tego wprowadzającego rozdziału. Należy jednak zwrócić uwagę, że podobnie jak w Qt, żadne widżety KDE nie są usuwane w destruktorze. Związane jest to z tym, że widżety KDE są wyprowadzone z Qt:
EditorWindow::~EditorWindow()
{
Widżet KEdit posiada znacznik modyfikacji (ang. modified flag). Użytkownik jest pytany, czy chce zapisać plik jedynie wtedy, gdy plik został zmodyfikowany:
if(m_pEditor->isModified())
{
KDE, podobnie jak Qt, udostępnia kilka odmian okien dialogu z użytkownikiem, za pomocą których można formułować zapytania w typowych sytuacjach wymagających potwierdzenia bądź anulowania jakiegoś działania. Tutaj użyjemy okna dialogowego z ostrzeżeniem z klasy KMessageBox, z przyciskami Yes i No:
int rc = KMessageBox::warningYesNo(
this,
"There are unsaved changes. Do you want to save\n"
"the changes?");
if(rc == KMessageBox::Yes)
saveFile();
}
}
Działanie New (Nowy) utworzenia nowego tekstu spowoduje wyczyszczenie widżetu edytora. Tak jak destruktor, musimy zadbać o zgodę użytkownika, w razie gdyby tekst nie jest zapisany:
void EditorWindow::newFile()
{
if(m_pEditor->isModified())
{
int rc = KMessageBox::warningYesNo(
this,
"There are unsaved changes. Are you sure you want to\n"
"start a new file?");
if(rc == KMessageBox::No)
return;
}
I teraz edytor może zostać oczyszczony. Należy też usunąć znacznik modyfikacji (edycji), gdyż brak tekstu jest zdefiniowany jako tekst, który nie jest edytowany. Zaniedbanie tego spowodowałoby, że przy zamykaniu aplikacji użytkownik byłby proszony o zapisanie pustego pliku.
m_pEditor->clear();
m_pEditor->setModified(false);
}
Dla działania Open (Otwórz) otwarcia pliku wykorzystamy dialog plikowy KDE (ang. KDE's file dialog) i poprosimy użytkownika o podanie nazwy pliku:
Rys., str. 471
|
KFileDialog jest dialogiem używanym w KDE do wyboru plików i katalogów. Jest podobny do QFileDialog z Qt, ale ma więcej możliwości oraz inny GUI. Udostępnia on kilka funkcji statycznych, łatwych w użyciu, takich jak te, które zostaną omówione za moment, jak również pozwala na otwarcie adresu URL i wybór wielu plików. Ciekawsza jest może zdolność określenia widżetu podglądu (ang. preview widget). Może to być wykorzystane do czegoś w rodzaju pokazywania miniaturek obrazków (ang. thumbnails of images):
void EditorWindow::openFile()
{
QString file = KFileDialog::getOpenFileName();
Jeśli użytkownik nie określił pliku, wówczas zwrócony zostanie pusty łańcuch. Sprawdzimy to tutaj:
if( ! file.isEmpty())
{
QFile f(file);
if(f.open(IO_ReadOnly))
{
Kiedy plik jest otwarty, możemy z niego odczytywać. W tym celu wykorzystamy klasę strumienia tekstowego (ang. text stream class), a konkretnie QTextStream. Jest to klasa zbliżona do klasy STL iostream. Z pewnych względów jednak użyjemy tutaj klasy strumienia z Qt.
Jeśli aplikacja byłaby stuprocentową aplikacją Qt, zmuszeni bylibyśmy samodzielnie odczytywać ze strumienia i umieszczać tekst w edytorze. Używamy jednak edytora KEdit z KDE z jego funkcjami bezpośredniego odczytu i zapisu do strumieni. To upraszcza nasz kod:
QTextStream s(&f);
m_pEditor->insertText(&s);
Jak uprzednio, usuwamy znacznik modyfikacji (ang. modified flag), ponieważ plik został właśnie załadowany z dysku:
m_pEditor->setModified(false);
}
}
}
Działanie Save (Zapisz) zapisu pliku jest podobne do otwarcia pliku (okno dialogowe jest identyczne):
void EditorWindow::saveFile()
{
QString file = KFileDialog::getSaveFileName();
if( ! file.isEmpty())
{
QFile f(file);
if(f.open(IO_WriteOnly | IO_Truncate))
{
QTextStream s(&f);
Ponownie zezwolimy widżetowi edytora zapisywać bezpośrednio do samego strumienia:
m_pEditor->saveText(&s);
m_pEditor->setModified(false);
}
}
}
I to jest okno główne w całej okazałości, z paskiem narzędzi, paskiem menu, edytorem i wszystkimi funkcjami! Ale to jeszcze nie koniec, bo jeszcze trzeba otworzyć to okno:
#include <kapp.h>
#include <kabaoutdata.h>
#include <kcmdlineargs.h>
#include "editorwindow.h"
int main(int argc, char **argv)
{
Począwszy od KDE 2, dla którego to środowiska tworzymy, aplikacje muszą mieć obiekt KAboutData. Klasa ta jest wykorzystywana do udzielania informacji o aplikacji, takich jak nazwa, opis, prawa autorskie i strona główna (ang. homepage). Używający wczesnej wersji KDE mogą natknąć się na ostrzeżenie, wbrew rzeczywistemu stanowi, o braku określonych danych:
KAboutData aboutdata(
"simplekeditor",
"Simple KDE Editor",
"1.0",
"Wrox Demo Application; Text Editor",
KAboutData::License_GPL,
"(c) 2000, Wrox Press",
"http://www.wrox.com",
"The KDE example application conceived in\n"
"'Professional Linux Programming'\n"
"Wrox Press 2000",
"none");
aboutdata.addAuthor("Marius Sundbakken");
KDE wymaga również przekazania parametrów wiersza poleceń do KCmdLineArgs. Tym sposobem nasza aplikacja automatycznie obsłuży kilka ogólnych argumentów, takich jak '—-help':
KcmdLineArgs::init(argc, argv, &aboutdata);
Używamy obiektu aplikacji KApplication, a nie QApplication , ponieważ jest to aplikacja KDE:
KApplication app;
Kolejne wiersze są podobne do tych, które już widzieliśmy:
EditorWindow w;
w.show();
return app.exec();
}
I to by było na tyle! Napisaliśmy prosty, nie mniej jednak w pełni funkcjonalny edytor tekstowy z możliwością zarówno ładowania jak i zapisu. Aplikacja rozpoznaje zmiany w konfiguracji KDE, takie jak zmiany wzornictwa pulpitu, czy zmiany czcionki. Przed przystąpieniem do kompilacji źródła, napiszemy jeszcze plik projektowy editor.pro:
TARGET = editor
MOC_DIR = moc
OBJECTS_DIR = obj
INCLUDEPATH = /opt/kde2/include
TMAKE_LIBDIR_X11 += -L$(KDEDIR)/lib
TMAKE_LIBS_X11 += -lkdeui -lkdecore -lkfile
SOURCES = main.cpp \
editorwindow.cpp
HEADERS = editorwindow.h
Jak zwykle, przetwarzamy go za pomocą tmake, kompilujemy i uruchomiamy:
$ tmake editor.pro -o Makefile
$ make
$ ./editor
A oto, co otrzymujemy:
Rys., str. 474
|
Materiały źródłowe
Qt można odnaleźć na stronie WWW: http://www.trolltech.com/
Lista korespondencyjna Qt: http://qt-interest.trolltech.com/
Lista często zadawanych pytań (FAQ) na temat instalacji Qt: http://www.trolltech.com/developer/faq/install.html
KDE można znaleźć na stronie WWW: http://www.kde.org/
Lista korespondencyjna KDE: http://lists.kde.org/
Istnieją dwie grupy dyskusyjne — angielska: comp.os.windows.x.kde i niemiecka: de.alt.comp.kde
Qt Architect: http://qtarch.sourceforge.net/
QtEZ: http://qtez.ibl.sk/
KDE Studio: http://www.thekompany.com/projects/kdestudio/
KDevelop: http://www.kdevelop.org/
Podsumowanie
W tym rozdziale wykonaliśmy kilka podstawowych aplikacji Qt. Przedstawiliśmy stosunkowo szczegółowe wyjaśnienia na temat systemu sygnałów i szczelin (SIGNAL-SLOT), widżetów (ang. widgets) i układów (ang. layouts). Przedyskutowaliśmy, jak można wykorzystać tmake. Choć materiał był niezbyt obszerny, powinien wystarczyć Czytelnikowi do wkroczenia z powodzeniem w świat programowania Qt.
Wreszcie, opracowaliśmy prostą aplikację KDE i przedstawiliśmy kilka zagadnień związanych z KDE. Wyczerpanie tego tematu wymagałoby napisania co najmniej jednej oddzielnej książki, a nie połowy skromnego rozdziału. Jednak omówione tutaj zagadnienia dają Czytelnikowi podstawy do poszerzenia zdobytej wiedzy o nowe tematy, wśród których polecamy:
Kconfig.
Analizator składniowy pliku konfiguracyjnego KDE.
KbugReport.
Okno dialogowe dla użytkowników do wysyłania sprawozdań o błędach.
Wszechstronna obsługa dokowania (extensive docking support), umożliwiająca użytkownikowi reorganizację interfejsu za pomocą metody „przeciągnij i upuść” (`drag and drop').
System XMLGUI, umożliwiający budowę GUI z dokumentu XML.
Analiza składniowa wiersza poleceń.
Komunikacja między procesami (interprocess communication).
I znacznie więcej.
Czytelnik powinien, do czego usilnie namawiamy, zajrzeć do cytowanej dokumentacji dotyczącej Qt i KDE.
Rysunek Pana Pingwina z fajką, str.476.
Dyskusja online na stronie WWW: http://www.p2p.wrox.com
2 Część I ♦ Podstawy obsługi systemu WhizBang (Nagłówek strony)
2 C:\Robert\Helion\plp13_c.doc
QObject
QApplication
QWidget
QButton
QMainWindow
QDialog
QPushButton
QTabDialog