14. Interfejs GUI dla Filmoteki DVD — implementacja w KDE i Qt
W tym rozdziale zajmiemy się ponownie implementacją interfejsu użytkownika dla Filmoteki DVD (ang. DVD Store) — programu obsługującego wypożyczalnię płyt DVD z filmami. Tym razem GUI zostanie utworzony przy użyciu KDE i Qt. By nie utrudniać zadania na samym początku, posługiwać się będziemy wyłącznie zestawem narzędzi Qt. W ten sposób wykażemy podobieństwo używania Qt i KDE, zwiększając przy tym właściwość łatwego przenoszenia miedzy platformami systemowymi. Zakładamy, że Qt jest obsługiwane na większej liczbie platform niż KDE. Później zademonstrujemy, w jaki sposób można skłonić aplikację do wykorzystania KDE.
W rozdziale zademonstrujemy:
przegląd niektórych dialogów;
wykorzystanie biblioteki bazy danych (ang. database library) w naszym GUI;
adaptację wersji GUI, opartej wyłącznie na Qt, tak aby wykorzystywała w części możliwości KDE.
Należy zwrócić uwagę na fakt, że w tym rozdziale nie umieścimy kompletnego wydruku kodu. Kod ów jest, podobnie jak wszystkie inne kody w tej książce, dostępny do pobrania ze strony WWW: www.wrox.com.
Projekt aplikacji
Na ile to możliwe postaramy się, aby różnice w zestawie dostępnych funkcji pomiędzy tworzoną przez nas w tym rozdziale wersją aplikacji a jej oryginalną wersją w GNOME/GTK+, były nieznaczne. Chcemy w ten sposób ułatwić Czytelnikowi ich porównanie. Zamierzamy uwzględnić następujące cechy w projektowanej aplikacji:
okno dialogowe dostępu do bazy danych (ang. database login dialog),
dziennik zdarzeń uwidaczniający każdą transakcję,
dodawanie, edycja i usuwanie klientów wypożyczalni,
dodawanie, edycja i usuwanie tytułów,
dodawanie płyt do tytułów,
wyszukiwanie klientów wypożyczalni i tytułów,
sprawdzanie statusu wypożyczenia płyty (wyszukiwanie płyt),
wypożyczanie filmów klientom wypożyczalni,
zwracanie poszczególnych płyt, wyświetlanie informacji o ich ewentualnym przetrzymaniu,
rezerwacja tytułów,
zapis konfiguracji.
Z zapleczem w postaci napisanej już bazy danych, nasze zadanie sprowadza się głównie do utworzenia GUI. Wykonanie wszystkich powyższych funkcji będzie możliwe z oknem głównym, wyposażonym w pasek menu i pasek narzędzi. Ponadto powinniśmy obsłużyć następujący zestaw funkcji:
Użytkownik może dodać nowych klientów, tytuły i płyty otwierając z paska menu odpowiednio okno dialogowe klienta (ang. member dialog), okno dialogowe tytułu (ang. title dialog) lub okno dialogowe płyty (ang. disk dialog). Klienci mogą również zostać dodani z paska narzędzi.
Trzy różne wyszukiwania są zebrane w jedno okno dialogowe wyszukiwań (ang. search dialog).
Te same okna dialogowe są wykorzystywane do edycji danych klienta i tytułu. W obu przypadkach są wywołane z okna dialogowego wyszukiwań.
Użytkownik za pomocą kliknięcia prawym przyciskiem myszy jakiegoś elementu może otworzyć menu rozwijane (ang. popup menu) z opcjami edycja (ang. edit) i usuń (ang. delete).
Użytkownik może wywołać zarówno z paska menu jak i paska narzędzi osobne okna dialogowe dla wypożyczeń (ang. renting), zwrotów (ang. returning) i rezerwacji (ang. reserving) tytułów.
Okno dialogowe dla zwrotów otworzy drugie okno dialogowe ukazujące, czy któraś ze zwróconych płyt była przetrzymana.
I wreszcie, użytkownik w oknie dialogowym konfiguracji (ang. configuration dialog) będzie mógł określić różne opcje, takie jak nazwa pliku dziennika zdarzeń (ang. log file name).
Będziemy używać następujących widżetów:
Nazwa widżetu |
Typ widżetu |
Opis |
ConnectDialog |
QDialog |
Okno dialogowe rejestracji (ang. login dialog). Ustali połączenie do bazy danych i przygotuje aplikację do użycia. Jedyną dostępną funkcją przed ustaleniem połączenia jest okno ustawień (ang. preferences window). |
DiskSearchPage |
QWidget |
Strona wyszukiwań płyt (ang. search page for disks). Okno dialogowe, w którym użytkownik może sprawdzić status wypożyczenia płyty. |
DVDSearchPage |
QWidget |
Strona wyszukiwań płyt DVD (ang. search page for DVDs). Tutaj użytkownik może wyszukiwać płyt DVD według tytułu i (lub) aktora i (lub) reżysera. Okno dialogowe działa też jako punkt wyjściowy edycji i usuwania tytułów. |
GeneralPage |
QWidget |
Strona ustawień ogólnych (ang. general preferences page). Tutaj użytkownik może określić nazwę dziennika zdarzeń i ustalić maksymalny okres wypożyczenia po upływie którego wypożyczona płyta jest uznana za przetrzymana. Jest to pierwsza z dwóch stron w oknie ustawień (ang. preferences window). |
MainWindow |
QMainWindow |
Okno główne (ang. main window). Początkowe okno aplikacji. Większość działań będzie stąd wywołana za pomocą paska menu albo paska narzędzi. To okno posiada również dziennik transakcji i wykaz wypożyczeń. |
MemberDialog |
QDialog |
Okno dialogowe dodawania klientów wypożyczalni (ang. dialog for adding members). To okno dialogowe może być wywołane z okna głównego, gdzie pełni funkcję okna dialogowego dodawania nowych klientów wypożyczalni. Wywołane z okna dialogowego wyszukiwań (ang. search dialog) może posłużyć użytkownikowi do edycji danych istniejącego klienta wypożyczalni. |
MemberSearchPage |
QWidget |
Strona wyszukiwań klientów (ang. search page for members). Tutaj użytkownik może wyszukiwać klientów według numeru członkowskiego wypożyczalni, lub według nazwiska. Tutaj też odbędzie się edycja i usuwanie klientów wypożyczalni. |
PreferencesDialog |
QTabDialog |
Okno dialogowe ustawień (ang. preferences dialog). Jest to okno dialogowe z dwoma stronami zawierającymi różne opcje do ustawienia (ang. adjustable options). |
RentDialog |
QDialog |
Okno dialogowe wypożyczeń (ang. rental dialog). Z tego okna dialogowego użytkownik określa, jakie płyty klient zamierza wypożyczyć. |
RentedDialog |
QDialog |
Okno dialogowe wyników wypożyczeń (ang. rental results dialog). To okno dialogowe pojawia się, gdy użytkownik zakończył działanie w oknie dialogowym wypożyczeń. Pokazuje adres użytkownika i wykaz tytułów, które klient zamierza wypożyczyć oraz które płyty są do wypożyczenia. Będzie również pokazane, jeśli tytuł jest niedostępny (wszystkie płyty są wypożyczone). |
ReserveDialog |
QDialog |
Okno dialogowe rezerwacji (ang. reserve dialog). Tutaj użytkownik dokonuje rezerwacji tytułu dla klienta. |
ReturnDialog |
QDialog |
Okno dialogowe zwrotów (ang. return dialog). W tym oknie dialogowym użytkownik zgłosi fakt zwrotu pożyczonych przez klienta płyt. |
SearchDialog |
QTabDialog |
Okno dialogowe wyszukiwań (ang. search dialog). W tym oknie dialogowym znajdują się trzy strony dla wyszukiwań. |
SearchWindowPage |
QWidget |
Strona ustawień wyszukiwań (ang. search preferences page). Jest to druga strona z dwóch dostępnych w oknie ustawień. Tutaj użytkownik może uaktywnić i wyłączyć różne pola na stronie wyszukiwań klienta i tytułu, tak aby ją dostosować do swoich wymagań. |
TitleDialog |
QDialog |
Okno dialogowe dodawania nowych płyt DVD (ang. dialog for adding new DVDs). To okno dialogowe, gdy wywołane z okna głównego, posłuży do wpisu nowych tytułów. Wywołane ze strony wyszukiwań tytułów, będzie działać jako okno dialogowe do edycji tytułu. |
No class |
QInputDialog |
Okno dialogowe dodawania płyt do tytułów (ang. dialog for adding disks to titles). To okno używa wbudowanego okna dialogowego Qt. |
Na diagramie poniżej uwidoczniono relacje między różnymi oknami dialogowymi i ich związek, jeśli taki istnieje, z zapleczem bazy danych (ang. database backend). Okno dialogowe ustawień jest na diagramie uproszczone (nie są pokazane jego obie strony). Ukazane są natomiast obie strony okna dialogowego wyszukiwań. Nie są pokazane związane z rejestracją relacje okna głównego.
Nie wszystkie fragmenty kodu użyte w tym rozdziale będą bezpośrednio związane z KDE lub Qt. Czytelnik może również samodzielnie dojść do lepszych rozwiązań. My skupimy się przed wszystkim na kodzie KDE i Qt, a dla innych problemów, takich jak na przykład zapisywanie w dzienniku (ang. logging), wybraliśmy proste rozwiązania — tak, aby nie przysłonić zasadniczego tematu tego rozdziału. Z tego też powodu, niektóre przyjęte uproszczenia mogą nie pasować do w pełni rozwiniętej, dojrzałej aplikacji.
Zaczniemy od opisu rozmaitych części GUI. Nie będzie to z pewnością opis w pełni wyczerpujący. Celowo staraliśmy się jednak omówić mniej więcej wszystkie okna dialogowe, przedyskutowane w rozdziale o implementacji w GNOME/GTK+ tak, aby umożliwić Czytelnikowi porównanie obu tych realizacji. Omawiane okna dialogowe obejmują również większość używanych tu funkcji Qt. Stanowi to wszystko zgrabny fragment aplikacji, wart poznania. W podrozdziale dotyczącym Qt omówimy:
okno główne
okno dialogowe klienta
okno dialogowe wypożyczeń
okno dialogowe raportów wypożyczeń (ang. rental report dialog)
okno wyszukiwań i jego strony
menedżera ustawień
Okno główne
W oknie głównym powinien znajdować się pasek menu umożliwiający podejmowanie różnych działań. Byłoby pożyteczne umieścić tam też pasek narzędzi z najczęściej realizowanymi zadaniami. Zasadniczy obszar okna zajmować będzie okno dziennika zdarzeń (ang. log window) i wykaz wypożyczeń (ang. loan list). Chcemy, żeby były to oddzielne struktury, umieścimy je więc na osobnych stronach, używając w tym celu widżetu zakładki (ang. tab widget). Mając gotowe okno główne możemy przystąpić do dodawania rozmaitych okien dialogowych, potrzebnych do realizacji innych funkcji aplikacji.
Spójrzmy przez chwilę na plik nagłówkowy:
class MainWindow :
public QMainWindow
{
Q_OBJECT public:
MainWindow();
virtual ~MainWindow();
Jak wiemy z poprzedniego rozdziału, makrodefinicja Q_OBJECT jest wymagana we wszystkich klasach, wykorzystujących sygnały i (lub) szczeliny.
Jednym z wymogów aplikacji jest implementacja dziennika transakcji (ang. transaction log), który będzie używany głównie do zgłaszania sprawozdań o wynikach modyfikacji bazy danych. Istnienie możliwości dodawania (wpisów) do dziennika zdarzeń (ang. log) z każdego miejsca w aplikacji jest konieczne, ponieważ każde okno dialogowe ma swój własny kod do obsługi bazy danych.
Jest wiele sposobów, by podołać temu zadaniu. My umieścimy dziennika zdarzeń w oknie głównym i dodamy funkcję statyczną do klasy okna głównego:
static void addLog(const QString& msg);
A teraz przejdziemy do szczelin (ang. slots):
private slots:
void connectDatabase();
void disconnectDatabase();
void addMember();
void addTitle();
void addDisk();
void find();
void rentDVD();
void returnDVD();
void reserve();
void preferences();
void about();
void shutdown();
...
}
Te szczeliny są ściśle skorelowane z wykazem cech naszej przyszłej aplikacji. Można je wdrażać w mniej, lub bardziej arbitralnej kolejności, przy czym kod połączenia z bazą danych musi być opracowany w pierwszej kolejności. Ustawienia (ang. preferences) są całkowicie niezależne od bazy danych. Okno dialogowe ustawień omówimy tylko pobieżnie. Jest to bowiem całkiem proste zagadnienie, z jedynie komponentami GUI oraz niewielką ilością dodatkowego kodu.
Spójrzmy na okno główne. Otwiera się go w taki sam sposób, jak demonstrowany w poprzednim rozdziale:
MainWindow::MainWindow() :
QMainWindow(), "dvdstore")
{
setCaption("DVDStore");
m_pStoreMenu = new QPopupMenu(this);
W ten sposób ustawiliśmy opis okna ("DVDStore") i utworzyliśmy nowe menu dla naszego paska menu. Wykorzystamy w tym celu klasę QPopupMenu, która może również być użyta do utworzenia zależnych od kontekstu menu rozwijanych (ang. context-sensitive popup menus). Przy omawianiu okna dialogowego wyszukiwań zapoznamy się z metodą tworzenia tego typu menu.
Jak zwykle należy określić macierzysty obiekt (ang. parent) poprzez przekazanie obiektu okna głównego do konstruktora QPopupMenu. Połączenie z bazą danych wymaga elementu menu oraz przycisku paska narzędzi.
Elementy menu
Najpierw utworzymy element menu (ang. menu item):
m_ConnectItem = m_pStoreMenu->insertItem(
"&Connect...",
this,
SLOT(connectDatabase()));
Funkcja zwraca identyfikator wstawionego elementu. Zazwyczaj można go zlekceważyć, ale nie tym razem, ponieważ będziemy chcieli tak aktywować, jak i wyłączać ten element. Nie jest zbyt sensowne oferowanie opcji połączenia z bazą danych, kiedy już jesteśmy z nią połączenia. Jak tylko połączenie z bazą danych będzie nawiązane, wyłączymy ten element via identyfikator ID (jak to zobaczymy później), zapisany w m_ConnectItem.
insertItem
Jest to funkcja istniejąca w wielu wariantach. Poniższy kod dodaje element menu z klawiszem skrótu:
m_AddMemberItem = m_pStoreMenu->insertItem(
"Add Member...",
this,
SLOT(addMember()),
CTRL + Key_M);
Reszta menu jest tworzona dokładnie w taki sam sposób.
Kiedy wszystkie menu są utworzone, dołączamy je do paska menu. Klasa QMainWindow, której używamy, udostępnia nam pasek menu i nie musimy tworzyć go samodzielnie. Pozostaje nam tylko dołączenie naszych menu do paska menu:
menuBar()->insertItem("&DVDStore", m_pStoreMenu);
Element menu jest domyślnie aktywny. Jak już wspomnieliśmy identyfikator elementu menu będzie służyć do aktywowania i wyłączania działania elementu. Dla przykładu, aby wyłączyć działanie elementu menu połączenia z bazą danych:
m_pStoreMenu->setItemEnabled(m_ConnectItem, false);
Pasek narzędzi
Jest nam potrzebny także pasek narzędzi (ang. toolbar) i dzięki klasie QToolBar możemy go mieć:
m_pToolBar = new QToolBar("toolbar", this);
Następnie można użyć klasy QToolButton w celu dodania przycisków do paska narzędzi:
m_pConnectButton = new QToolButton(
Qpixmap(DVDSTORE_ICON_CONNECT),
"Connect",
"Connect to database",
this,
SLOT(connectDatabase()),
m_pToolBar);
Należy zapamiętać, że każdy widżet może być umieszczony w pasku narzędzi, ale sam pasek narzędzi może jedynie znaleźć się w obiekcie QMainWindow.
Nasz przycisk narzędziowy (ang. tool button) odpowiada elementowi menu Connect... . Pierwsze dwa argumenty określają mapę pikselową dla przycisku narzędzi i etykietę tekstową "Connect", która będzie pokazana poniżej ikony. Łańcuch "Connect to database" jest tekstem do pokazania na pasku stanu, kiedy wskaźnik myszy znajdzie się nad przyciskiem. Dwa kolejne wiersze są dokładnie takie same, jak te w funkcji insertItem dla elementów menu. Na zakończenie określamy pasek narzędzi, na którym przycisk ma zostać umieszczony.
Mając zbudowany pasek narzędzi możemy przystąpić do wstawienia go w nasze okno główne. Powinny być też ukazane etykiety tekstowe, które robi się tak:
addToolBar(m_pToolBar);
setUsesTextLabel(true);
Należy zapamiętać, że obie powyższe funkcje są składowymi naszej klasy podstawowej (ang. base class) QMainWindow. Pamiętać też warto, że bardzo łatwo można wyłączyć działanie przycisków narzędziowych:
m_pDisconnectButton->setEnabled(false);
oraz utworzyć separatory paska narzędzi za pomocą takiego wiersza:
m_pToolBar->addSeparator();
Centralny widżet
Mając utworzony pasek menu i pasek narzędzi, możemy przejść do centralnej (środkowej) części okna. Chcemy tu umieścić dwie zakładki, pierwszą zawierającą rejestr zdarzeń i drugą z wykazem wypożyczeń (ang. loan list). Zakładki tworzy się za pomocą klasy QTabWidget:
m_pCentralWidget = new QTabWidget(this);
Dla wydruku rejestru zdarzeń użyjemy widżetu edytora tekstu (ang. text editor widget), dostarczonego przez Qt, w trybie tylko do odczytu.
W rzeczywistości Qt dostarcza bardziej adekwatny widżet do tego typu rejestrów zdarzeń. Jest nim widżet tylko do odczytu ze znacznikiem RichText, zwany QTextBrowser. My wybraliśmy jednak edytor tekstu, by pozostać jak najbliżej wersji GNOME/GTK+:
m_pLog = new QMultiLineEdit(m_pCentralWidget);
m_pLog->setReadOnly(true);
Należy zwrócić uwagę na zmianę w polu widżetu macierzystego (ang. parent field) — nie jest to już okno główne. Ponieważ rejestr zdarzeń ma znaleźć się wewnątrz widżetu zakładki, widżetem macierzystym jest teraz widżet zakładki (ang. tab widget).
Dla wykazu wypożyczeń zamierzamy użyć klasy QlistView. Tak jak w przypadku edytora, wykaz znajdzie się wewnątrz widżetu zakładki, dlatego widżet zakładki przekazujemy do konstruktora wykazu (ang. list constructor). Następnie tworzymy w wykazie kilka kolumn:
m_pList = new QListView(m_pCentralWidget);
m_pList->addColumn("Member No.");
m_pList->addColumn("DVD");
m_pList->addColumn("Title");
m_pList->addColumn("Due Back");
Mając gotową zawartość widżetu zakładki możemy przypisać zakładkę dla rejestru zdarzeń i wykazu wypożyczeń:
m_pCentralWidget->addTab(m_pLog, "DVDStore"); m_pCentralWidget->addTab(m_pList, "On Loan");
Pozostały jeszcze tylko dwie sprawy do załatwienia i nasze okno główne będzie gotowe. Po pierwsze, należy go zawiadomić, że nasz widżet zakładki będzie jego centralnym widżetem, a po drugie, musimy mu nadać rozsądny rozmiar początkowy (ustawienie pozycji można pozostawić w gestii menedżera okien):
setCentralWidget(m_pCentralWidget);
resize(460, 300);
}
Rys., str.485
|
Rejestr transakcji
Należy dołożyć wszelkich starań, aby zapewnić bezpieczne przechowywanie rejestru transakcji (ang. transaction log) oraz aby uległo utracie jak najmniej informacji w przypadku krachu aplikacji. Zastosowane tutaj rozwiązanie nie jest oczywiście idealne, ale jak już dyskutowaliśmy to uprzednio, będziemy koncentrować uwagę na po stronie Qt. Z tego właśnie powodu zostało wybrane najprostsze rozwiązanie. Rejestr będzie zapisany tylko wtedy, gdy aplikacja będzie odpowiednio zamknięta, tzn. nie będzie krachu aplikacji.
Zamierzamy umieścić zapisywanie pliku rejestru w destruktorze okna głównego. Użyjemy klasy QFile i zezwolimy strumieniowi tekstu QTextStream pracować na tym pliku. Jest to częsty sposób pracy na plikach. Dla rejestru użyjemy prostego pliku tekstowego ASCII.
MainWindow::~MainWindow()
{
QString logfile = SettingsManager::instance()->getString("logfile");
if(logfile == "")
logfile = "logfile.txt";
QFile f(logfile);
if(f.open(IO_WriteOnly | IO_Raw | IO_Append))
{
QTextStream s(&f);
s << m_pLog->text();
s << "\n";
f.close();
}
}
Kod destruktora dogląda, czy została określona nazwa pliku rejestru (jeśli nie, używa nazwy domyślnej) i przekazuje ją do konstruktora QFile, który otwiera plik w trybie „zapisz i dopisz” (ang. `write and append'), bez żadnego buforowania.
Jak widać, strumienie pracują w mniej więcej taki sposób, jak pracują standardowe strumienie wejścia-wyjścia. Można zastanawiać się, dlaczego powinniśmy używać klas Qt? Pierwszym powodem jest łatwość przenoszenia (ang. portability). Z wyjątkiem używanej przez nas biblioteki bazy danych, cała nasza aplikacja może zostać skompilowana na platformie systemowej Windows bez żadnej zmiany w kodzie! Drugi powód jest taki, że jeśli pozostaje się przy Qt to nie ma potrzeby dołączania innych bibliotek, gdyż najprawdopodobniej wszystko, co jest niezbędne, można znaleźć właśnie w Qt.
Łatwiejsze wydawać się też może rozszerzenie strumieni Qt, gdyż Qt działa na klasie QIODevice, dla której można utworzyć podklasę. Natomiast standardowy iostream nie działa na takiej klasie. Utworzenie otoczek cout, cin i cerr jest tylko kwestią wpisania:
QTextStream cout(stdout, IO_WriteOnly);
QTextStream cin(stdin, IO_ReadOnly);
QTextStream cerr(stderr,IO_WriteOnly);
Można również, w miarę potrzeb, pracować na obiektach QString i tablicach bajtów (ang. byte arrays) z pomocą klas strumieni.
Okno dialogowe klienta
W oknie dialogowym klienta dodaje się nowych klientów do aplikacji. Wymaga to dostępu do zaplecza bazy danych. Dlatego użyjemy biblioteki C, która była zaimplementowana w rozdziale 4. Ponieważ używamy języka C++, będziemy musieli dodać cienką otoczkę wokół pliku nagłówkowego dvd.h. Nazwiemy to qdvd.h:
#ifdef __cplusplus
extern "C"
{
#endif
#include "dvd.h"
#ifdef __cplusplus
}
#endif
Plik memberdialog.h wygląda tak:
private slots:
void okay();
private:
QLineEdit *m_pTitle;
QLineEdit *m_pFirstName;
QLineEdit *m_pLastName; #include <qdialog.h>
#include <qlineedit.h>
class MemberDialog : public QDialog
{
Q_OBJECT
public:
MemberDialog(const char *member_no, QWidget *parent = 0,
const char *name = 0);
virtual ~MemberDialog();
QLineEdit *m_pHouseNum;
QLineEdit *m_pAddr1;
QLineEdit *m_pAddr2;
QLineEdit *m_pTown;
QLineEdit *m_pState;
QLineEdit *m_pZip;
QLineEdit *m_pPhone;
QlineEdit *m_MemberNo;
};
W szczelinie okay skopiujemy cały wprowadzony tekst z wierszy edycji i umieścimy go w strukturze, którą przekażemy do bazy danych. Oto memberdialog.cpp:
MemberDialog::MemberDialog(
const char *member_no,
QWidget *parent,
const char *name)
: QDialog(parent, name, true)
{
Przekazujemy argument true jako argument końcowy do konstruktora QDialog, co oznacza, że okno dialogowe klienta będzie modalne. Jest to dla nas prostsze rozwiązanie, choć alternatywne okno niemodalne daje użytkownikowi potencjalnie większą swobodę.
Do rozmieszczenia widżetów został wybrany układ pionowy, z obramowaniem o szerokości czterech pikseli, i z czteropikselowymi odstępami:
QVBoxLayout *main = new QVBoxLayout(this, 4, 4);
Identyfikacja klientów odbywa się według nazwiska i adresu. W przyjaznym użytkownikowi interfejsie GUI chcemy podkreślić znaczenie tego naturalnego podziału na grupy. Wokół każdej grupy będzie obramowanie, utworzone za pomocą klasy QGroupBox lub jednej z jej pochodnych. Wykorzystamy odmianę poziomego grupowania, QHGroupBox:
QHGroupBox *namebox = new QHGroupBox("Name", this);
We wnętrzu tego okna planujemy kolumnę po lewej stronie z etykietami opisującymi jednowierszowe pola edycji w kolumnie po prawej stronie. Wybraliśmy siatkę (ang. grid), a dokładniej QGrid, aby wszystko było schludnie wyrównane. Wewnątrz pola grupy nie można użyć układów (ang. layouts):
QGrid *topgrid = new QGrid(2, namebox);
topgrid->setSpacing(4);
Utworzyliśmy siatkę z dwoma kolumnami (i dowolną liczba wierszy). Siatka jest wewnątrz okna namebox, ponieważ jest ono oknem macierzystym siatki. Ustaliliśmy też czteropikselowe odstępy między składnikami siatki.
Siatka jest wypełniana zwyczajnie poprzez te widżety, które mają siatkę jako swój widżet macierzysty:
(void) new Qlabel("Title:", topgrid);
m_pTitle = new QLineEdit(topgrid);
m_pTitle->setMaxLength(PERSON_TITLE_LEN - 1);
...
}
Te dwa widżety wypełnią teraz pierwszy wiersz siatki. Siatka będzie się dalej wypełniać w taki sposób:
1 |
2 |
3 |
4 |
5 |
6 |
... |
|
Należy zauważyć, że ponieważ nie ma potrzeby przechowywania obiektu etykiety, użyliśmy (void) przed new. Wszystkie widżety mające widżet macierzysty muszą być tworzone na stosie (ang. heap). W sytuacji, gdy zostanie usunięty widżet macierzysty, wszystkie widżety potomne będą także usunięte. My nie musimy jednak ich dealokować. Nie przechowujemy obiektu etykiety, ponieważ nie jest nam potrzebny dostęp do obiektu QLabel. Użycie (void) w tej postaci jest przydatne do tego celu od czasu do czasu.
Rys., str. 488.
|
Rozplanowaliśmy obiekty sterowania — gdy użytkownik kliknie przycisk Okay, to Qt wywoła naszą szczelinę okay. Kolejnym krokiem będzie zebranie wartości z GUI i umieszczenie ich w bazie danych. Wszystkie wierszowe pola edycji otrzymały maksymalną długość (zgodnie z rozmiarem odpowiadających im pól w strukturze, która mamy zamiar wypełnić). Nie ma więc potrzeby sprawdzania długości pól tekstowych:
void MemberDialog::okay()
{
dvd_store_member new_member;
int member_id, rc;
dvd_member_get_id_from_number(m_MemberNo, &member_id);
dvd_member_get(member_id, &new_member);
strcpy(new_member.title, m_pTitle->text().local8bit());
strcpy(new_member.fname, m_pFirstName->text().local8bit());
strcpy(new_member.lname, m_pLastName->text().local8bit());
strcpy(new_member.house_flat_ref, m_pHouseNum->text().local8bit());
strcpy(new_member.address1, m_pAddr1->text().local8bit());
strcpy(new_member.address2, m_pAddr2->text().local8bit());
strcpy(new_member.town, m_pTown->text().local8bit());
strcpy(new_member.state, m_pState->text().local8bit());
strcpy(new_member.zipcode, m_pZip->text().local8bit());
strcpy(new_member.phone, m_pPhone->text().local8bit());
Tutaj wypełniamy strukturę dvd_store_member wartościami z GUI. Teraz, kiedy mamy już strukturę nowego klienta, musimy sprawdzić, czy wprowadza ona zmianę w strukturze istniejącego klienta, czy jest to zupełnie nowy klient. Zmienna m_MemberNo zawiera numer klienta, jeśli taki istnieje, nadany w konstruktorze:
if( ! m_MemberNo.isEmpty())
{
rc = dvd_member_set(&new_member);
}
else
{
rc = dvd_member_create(&new_member, &mem_id);
Przekazujemy strukturę do biblioteki bazy danych, która zwraca identyfikator klienta (ID) dla nowego klienta wraz z innymi danymi. Jeśli funkcja zawodzi, wyświetlamy komunikat o błędzie (kodu tego nie pokazano):
if(rc == DVD_SUCCESS)
{
QString str = QString("%1 %2 %3 added as new member. Member no. %4")
.arg(m_pTitle->text()).arg(m_pFirstName->text())
.arg(m_LastName->text().arg(mem_id);
Dodajemy komunikat do dziennika zdarzeń, a także ukazujemy komunikat dziennika zdarzeń w oknie dialogowym:
MainWindow::addLog(str);
QMessageBox::information(this, "DVDStore", str,
QMessageBox::Ok. | QMessageBox::Default);
}
else
{
...
}
}
accept();
}
To powinno zamknąć okno dialogowe, oraz przekazać do programu wywołującego wartość zwracaną, która określa czy operacja zakończyła się pomyślnie. Użytkownik może zamiast tego, w przypadku błędów lub anulowania, wywołać reject. To również zamknie okno dialogowe, ale przekaże inną wartość zwracaną.
Okno dialogowe wypożyczeń
Okno dialogowe wypożyczeń (ang. rent dialog) obsługuje wypożyczanie tytułów DVD. Użytkownik może wywołać ten dialog z menu, paska narzędzi, albo z menu rozwijanego okna wyszukiwań płyt DVD. Zawiera ono pole, w którym użytkownik może podać identyfikator (ID) klienta, oraz wykaz do którego użytkownik może dodać numery identyfikacyjne tytułów, jakie klient zamierza wypożyczyć. Dla zwiększonego komfortu użytkownika, w oknie umieszczono przycisk, umożliwiający usunięcie wpisów z wykazu, gdyby we wpisie pojawiła się jakaś literówka:
Rys., str. 489
|
Wykaz wypożyczeń
Zaczniemy od przyjrzenia się funkcji wykazu. Dysponujemy przyciskiem Add (Dodaj) połączonym ze szczeliną add:
void RentDialog::add()
{
dvd_title dvd;
dvd_title_get(m_pTitleId->text().toInt(), &dvd);
(void) new QListViewItem(m_pList,
m_pTitleId->text(),
dvd.title_text);
}
Wypełnimy strukturę dvd_title identyfikatorem ID tytułu, określonym przez użytkownika. Nastepnie dodajemy tytuł do wykazu. Kiedy użytkownik kliknie przycisk Rent, musimy przeglądnąć ten wykaz:
void RentDialog::rent()
{
dvd_store_member mem;
int member_id = m_pMember->text().toInt();
int rc = dvd_member_get(member_id, &mem);
if(rc != 0)
{
QMessageBox::warning(this, "DVDstore", "No such member");
}
Ten kod odzyskuje z bazy danych stosowne informacje, dotyczące klienta, określonego przez numer ID klienta wypożyczalni podawanego przez użytkownika. Sprawdzamy, czy klient rzeczywiście istnieje. Jeśli nie, wówczas występuje błąd. Tu trzeba zauważyć, że w rzeczywistości okno dialogowe nie zostaje zamknięte. Użytkownik ma dodatkową szansę wprowadzenia numeru klienta.
Jeśli klient istnieje, przeglądamy wykaz wbudowany w add. Chcemy pokazać okno dialogowe raportów wypożyczeń (również wtedy, gdy faktycznie dokonujemy wypożyczenia), bo okno powinno wiedzieć o wykazie wypożyczeń. Istnieje wiele sposobów, by go o tym powiadomić. My wybraliśmy metodę budowania wykazu identyfikatorów ID tytułów i przekazania go poprzez konstruktora do okna dialogowego raportów wypożyczeń.
Budujemy wykaz identyfikatorów ID tytułów przeglądając widok wykazu (ang. list view) w GUI:
QListViewItem *item = m_pList->firstChild();
To zwraca pierwszy element wykazu. Dla identyfikatora ID tytułu można użyć klasy Qt QValueList:
QValueList<int> titlelist;
while(item != 0)
{
titlelist.append(item->text(0).toInt());
item = item->nextSibling();
}
Teraz ukrywamy okno dialogowe wypożyczeń i pokazujemy okno dialogowe raportów wypożyczeń:
hide();
RentedDialog dlg(m_Member->text(), titlelist, this);
dlg.show();
Kiedy użytkownik zamyka RentedDialog zezwalamy na to i zamykamy:
accept();
Okno dialogowe raportów wypożyczeń
Jak widzieliśmy, okno dialogowe wypożyczeń aktywuje okno dialogowe raportów wypożyczeń. To ostatnie przegląda przekazany mu wykaz identyfikatorów ID tytułów i wypożycza każdy tytuł określonemu klientowi:
...
int disk_id, title_id, rc;
QListViewItem *item;
QString log;
for(uint i = 0; i < titlelist.count(); i++)
{
title_id = titlelist[i];
rc = dvd_rent_title(member_id, title_id, &disk_id);
item = new QListViewItem(list);
item->setText(0, QString::number(title_id));
item->setText(1, findTitle(title_id));
if(rc == DVD_SUCCESS)
{
item->setText(2, "OK");
log.sprintf("Rented disk %d to Member: %d",
disk_id,member_id);
MainWindow::addLog(log);
}
else
item->setText(2, "N/A");
item->setText(3, QString::number(disk_id));
}
Tak wygląda okno dialogowe raportów wypożyczeń:
Rys., str. 491.
|
Okno wyszukiwań
Ustaliliśmy na samym początku tego rozdziału, że użytkownik musi mieć możliwość wyszukiwania tytułów filmów DVD, płyt DVD z danymi tytułami oraz klientów wypożyczalni. To są zdecydowanie trzy odmienne warianty wyszukiwania i chcemy, by GUI je odzwierciedlał. Dlatego tworzymy jedno okno dialogowe wyszukiwań z trzema zakładkami. Okno dialogowe wyszukiwań jest zatem oknem dialogowym z zakładkami.
Każda strona wymaga kilku widżetów, dwie ze stron wymagają też rozwijanego menu, korzystając z którego wygodnie można będzie wypożyczyć, dokonać rezerwacji, przyjąć zwrot, czy nawet usunąć jakieś tytuły DVD. Jest to zatem najbardziej skomplikowane okno dialogowe tej aplikacji a zatem wymaga zwrócenia wyjątkowej uwagi.
Temat zakładek został krótko zasygnalizowany przy omawianiu okna głównego. Wtedy użyliśmy QTabWidget, a teraz zamierzamy wyprowadzić klasę pochodną z QTabDialog, ponieważ udostępnia ona przycisk Close (Zamknij). Zaczniemy od przyjrzenia się konstruktorowi, abyśmy zorientowali się, jak użyć QTabDialog:
#include "searchdialog.h"
#include "dvdsearchpage.h"
#include "membersearchpage.h"
#include "disksearchpage.h"
SearchDialog::SearchDialog(
QWidget *parent,
const char *name)
: QTabDialog(parent, name, true)
{
setOkButton("Close");
DVDSearchPage *page1 = new DVDSearchPage(this);
MemberSearchPage *page2 = new MemberSearchPage(this);
DiskSearchPage *page3 = new DiskSearchPage(this);
addTab(page1, "DVD Title");
addTab(page2, "Member");
addTab(page3, "Disk");
}
Widzimy, że każda zaznaczona zakładką strona (ang. tab page) jest widżetem. W naszym oknie głównym potrzebowaliśmy jedynie wielowierszowego pola edycji na jednej stronie i wykazu na drugiej, co oznaczało, że widżety mogły być dodawane bezpośrednio. W naszym oknie wyszukiwań będziemy potrzebować jednak więcej niż jednego widżetu na jedną stronę. Wobec tego, najlepszym rozwiązaniem będzie utworzenie nowego widżetu (wyprowadzonego z QWidget) i rozbudowanie go tak, jak rozbudowywaliśmy do tej pory okna dialogowe.
Należy pamiętać, że okno dialogowe jest także widżetem. Zatem konfigurowanie widżetu za pomocą układów i widżetów przebiega zgodnie z metodą wcześniej prezentowaną.
W naszym oknie dialogowym wyszukiwań utworzyliśmy trzy widżety — po jednym na każdą stronę okna.
Strona wyszukiwań płyt DVD
Strona wyszukiwań płyt DVD umożliwi użytkownikowi przeszukiwanie zasobów płyt DVD ze względu na tytuł i (lub) nazwisko reżysera i (lub) nazwisko aktora. I tym razem nie będziemy zajmować się rozmieszczeniem widżetów i przejdziemy bezpośrednio do procedury wyszukiwania. Poniżej, dla zorientowania się w działaniu GUI, zamieszczono zrzut ekranu zaznaczonej zakładką strony wyszukiwań:
Rys., str. 493
|
Przycisk Search jest połączony ze szczeliną search:
void DVDSearchPage::search()
{
m_pList->clear();
int *result;
int count, colno;
dvd_title dvd;
dvd_title_search(m_pSearchFor->currentText(),
m_pActor->currentText(),
&result,
&count);
Funkcja dvd_title_search wypełnia tablicę wynikową identyfikatorami ID tytułów, spełniającymi określone kryteria. Następnie dla każdego elementu tablicy sprawdzamy dokładną informację o każdym tytule tak, aby zapełnić GUI danymi:
QListViewItem *item;
for(int i = 0; i < count; i++)
{
dvd_title_get(result[i], &dvd);
item = new QListViewItem(
m_pList);
QString::number(dvd.title_id),
dvd.title_text);
Dwie pierwsze kolumny (Title ID i Title) (ID Tytułu i Tytuł) są zawsze obecne — użytkownik nie może ich zamknąć. Inne kolumny mogą być natomiast wyłączone z poziomu okna ustawień. Poprzez menedżera ustawień określamy, które kolumny pokazać, a które nie. Kolumny są identyfikowane dzięki numerom, dlatego należy śledzić numery kolumn, aby móc wprowadzić tekst do właściwej kolumny:
colno =2;
Wkrótce zaczniemy robić wszechstronny użytek z metody getBool menedżera ustawień. Wartości boolowskie są przechowywane jako łańcuchy "TRUE" i "FALSE", co jest dosyć niewydajne. Lepszym rozwiązaniem wydaje się mieć listę wartości boolowskich i listę łańcuchów w menedżerze ustawień:
if(sm->getBool("show_refnum"))
item->setText(colno++, dvd.asin);
if(sm->getBool("show_director"))
item->setText(colno++, dvd.director);
if(sm->getBool("show_genre"))
item->setText(colno++, dvd.genre);
if(sm->getBool("show_classif"))
item->setText(colno++, dvd.classification);
if(sm->getBool("show_actor1"))
item->setText(colno++, dvd.actor1);
if(sm->getBool("show_actor2"))
item->setText(colno++, dvd.actor2);
if(sm->getBool("show_reldate"))
item->setText(colno++, dvd.release_date);
if(sm->getBool("show_rentcost"))
item->setText(colno++, dvd.rental_cost);
}
Na koniec, zwolnienie pamięci alokowanej dla tablicy wynikowej:
free(result);
}
Użytkownik musi mieć możliwość edycji i usunięcia tytułów płyt DVD. Zaimplementujemy te funkcje w oknie wyszukiwań, dodając tam menu rozwijane z elementami Edit (Edycja) i Delete (Usuń). Dodamy również dla wygody dwie funkcje — funkcje Rent (Wypożycz) i Reserve (Zarezerwuj). Menu rozwijane pokazuje się, gdy użytkownik kliknie prawym przyciskiem myszy element listy. Należy zatem ustawić niewielkie menu w konstruktorze widżetu strony wyszukiwań:
m_pPopup = new QPopupMenu(this);
m_pPopup->insertItem("Rent...", this, SLOT(rent()));
m_pPopup->insertItem("Reserve...", this, SLOT(reserve()));
m_pPopup->insertSeparator();
m_pPopup->insertItem("Edit...", this, SLOT(edit()));
m_pPopup->insertItem("Delete", this, SLOT(deleteDvd()));
To jest menu rozwijane. A teraz trzeba oczekiwać aż użytkownik kliknie widok listy prawym przyciskiem myszy:
connect(
m_pList,
SIGNAL(rightButtonClicked(QListViewItem*, const QPoint&, int)),
this,
SLOT(showPopup(QListViewItem*, const QPoint&, int))
};
Ten sygnał ma bardziej skomplikowaną sygnaturę, niż to widzieliśmy uprzednio. W częściach SIGNAL i SLOT brak jest zmiennych. Zamiast nich występują typy zmiennych. Szczelina showpopup jest skonstruowana prosto i logicznie:
void DVDSearchPage::showPopup(QListViewItem *item, const QPoint &p,
int)
{
if(item)
m_pPopup->popup(p);
}
Dzięki sprawdzeniu czy element (item) jest różny od zera, unikamy pokazania menu w sytuacji, gdy użytkownik kliknie pusty obszar. Szczelina ta pokaże menu rozwijane, które utworzyliśmy w konstruktorze. W ten sposób połączyliśmy szczelinę z każdym elementem. Jeśli użytkownik wybierze Rent... (Wypożycz...), to wtedy rozpoczynamy wykonanie szczeliny rent:
DVDSearchPage::rent()
{
QListViewItem *item = m_pList->currentItem();
int title_id = item->text(0).toInt();
RentDialog dlg(title_id, this);
dlg.show();
}
Zbudowaliśmy okno dialogowe wypożyczeń tak, że akceptuje identyfikator ID, który następnie będzie umieszczony w oknie dialogowym wypożyczeń interfejsu graficznego użytkownika (GUI).
W ten sposób powstają menu rozwijane. Można tworzyć menu rozwijane zależne od kontekstu. Wiąże to się jedynie ze sprawdzeniem w szczelinie showPopup rodzaju elementu, który został kliknięty przez użytkownika.
Strona wyszukiwań klientów
Drugi z kolei wymóg wyszukiwania obejmuje umożliwienie użytkownikowi szukania klientów. Strona wyszukiwań klientów obsługuje zarówno opcję wyszukiwania według nazwiska, albo numeru klienta:
Rys., str. 495
|
Tutaj również szczelina search połączona jest z przyciskiem Search (Szukaj):
void MemberSearchPage::search()
{
m_pList->clear();
int *result;
int count = 0;
Użytkownik może zaznaczyć do wyszukiwań albo numer klienta, albo jego nazwisko. Niezależnie, który sposób obierze, my jako podstawę do budowy wykazu w GUI użyjemy tablicę identyfikatorów ID klienta
if(m_pSearchFor->currentItem() == 0)
{
// Search for member no.
result = (int*)malloc(1);
Tutaj używamy funkcji maloc, a nie new. W ten sposób interfejs z biblioteką języka C i pamięć będzie zwolniona za pomocą free:
int member_id;
int rc = dvd_member_get_id_from_number(m_pSearch->text(),
&member_id);
if(rc ==DVD_SUCCESS)
{
result[0] = member_id;
count = 1;
}
}
else
{
// Search for last name.
dvd_member_search(m_Search->().local8Bit(),
&result,
&count);
}
dvd_store_member mem;
QListViewItem *item;
int colno, rc;
SettingsManager *sm = SettingsManager::instance();
for(int i = 0; i < count; i++)
{
colno = 1;
rc = dvd_member_get(result[i], &mem);
if(rc == DVD_SUCCESS)
{
item = new QListViewItem(m_pList, mem.member_no);
if(sm->getBool("show_title"))
item->setText(colno++, mem.title);
...
Użytkownik może wybrać kolumny do pokazania w oknie dialogowym ustawień. Wypełnianie wykazu i tych kolumn odbywa się podobnie, jak w przypadku strony wyszukiwań płyt DVD.
Strona wyszukiwań płyt
Trzecim i ostatnim już wymogiem wyszukiwania jest opcja wyszukiwania rzeczywistych płyt. Ta opcja pozwoli na ustalenie kto, jeśli rzeczywiście miał miejsce taki fakt, wypożyczył dane płyty z wypożyczalni:
Rys., str. 497
|
W uzupełnieniu całości i dla porównania zamieścimy poniżej wydruk kodu wyszukiwania płyt. Jest on stosunkowo prosty (kolumny nie dają się konfigurować):
void DiskSearchPage::search()
{
m_pList->clear();
int title_id = m_pSearchFor->value();
int *result;
int count;
int rc = dvd_disk_search(title_id, &result, &count);
if(rc == DVD_SUCCESS)
{
dvd_title dvd;
dvd_title_get(title_id, &dvd);
QListViewItem *item;
char date_rented[9];
int r, member_id;
for(int i = 0; i < count; i++)
{
item = new QListViewItem( m_pList,
QString::number(title_id),
dvd.title_text,
QString::number(result[i]));
r = dvd_rented_disk_info(result[i],
&member_id,
date_rented);
if(r == DVD_SUCCESS)
item->setText(3, QString::number(member_id));
}
}
}
Menedżer ustawień
Możliwość trwałego zapisu konfiguracji dodaje naszej aplikacji wiele walorów. Innymi słowy, użytkownik może podać nazwę pliku dziennika zdarzeń oraz określić aktywne kolumny w oknie dialogowym wyszukiwań, a my chcemy aby te ustawienia były utrwalone do następnej sesji.
Zamierzamy użyć naszą własną klasę menedżera ustawień, tak jak w destruktorze okna głównego. Klasa menadżera ustawień działa jak oparta na słowach kluczowych, prosta tabela przeglądowa (ang. lookup table) ustawień. Chcemy, aby ustawienia były możliwe do odczytu i zapisu globalnie. Innymi słowami, klasa ta jest w istocie rzeczy słownikiem zawierającym udogodnienia eksploatacyjne dla przechowywania i odzyskiwania prostych typów danych. To wszystko, czego potrzebuje nasza aplikacja. Nie musimy zapisywać żadnych złożonych struktur danych, które nie byłyby już zapisane przez interfejs bazy danych.
Klasa SettingsManager jest zaimplementowana jako klasa pojedyncza (ang. singleton), w celu uzyskania czystego punktu dostępu globalnego. Klasa pojedyncza to taka klasa, która posiada tylko jedną kopię, do której zapewnia globalny punkt dostępu, osiągnięty przy użyciu statycznej funkcji instance i zachowaniu prywatności konstruktora.
Trzeba mieć świadomość faktu, że ta implementacja nie jest zbyt wydajna, Wynika to na przykład z tego, że wszystkie wartości są przechowywane wewnętrznie jako łańcuchy; nawet wartości boolowskie i liczby całkowite.
Na początek przyjrzyjmy się interfejsowi, aby zapoznać się z klasą:
#include <qmap.h>
class SettingsManager
{
public:
virtual ~SettingsManager();
static SettingsManager* instance();
bool isSet(const QString& key) const;
void set(const QString& key, bool value);
void set(const QString& key, const QString& value);
bool getBool(const QString& key) const;
QString getString(const QString& key) const;
void save();
void load();
private:
SettingsManager();
QMap<QString,QString> m_Settings;
static SettingsManager *m_pInstance;
};
Mogliśmy użyć standardowych klas szablonowych map<> zamiast QMap. Jest to jednak kurs Qt, a przy tym QDataStream oferuje nam przyjemną funkcję do zapisu QMap bezpośrednio na dysk. Jedna z funkcji set:
void SettingsManager::set(const QString& key, const QString& value)
{
m_Settings.replace(key, new QString(value));
}
Okno dialogowe ustawień jest tym miejscem, gdzie menedżer ustawień jest używany w szerokim zakresie. Zapisuje ono do menedżera ustawień oraz z niego odczytuje stan każdej swojej strony. Poniżej kilka przykładów dla odzysku stanu:
SettingsManager *sm = SettingsManager::instance();
m_pRefNum->setChecked(sm->getBool("show_refnum"));
m_pDirector->setChecked(sm->getBool("show_director"));
...
oraz dla przechowania stanu:
SettingsManager *sm = SettingsManager::instance();
sm->set("show_refnum", m_pRefNum->isChecked());
sm->set("show_director", m_pDirector->isChecked());
...
Teraz, kiedy wszystkie ustawienia znajdują się w jednym miejscu, dodanie implementacji ich trwałego przechowania nie powinno przedstawiać trudności. Przynajmniej w tej sytuacji, gdzie nie mamy do czynienia z żadnymi złożonymi strukturami danych. Implementujemy podstawowe funkcje save i load:
void SettingsManager::save()
{
QFile f("settings");
if(f.open(IO_WriteOnly | IO_Raw | IO_Truncate))
{
QDataStream s(&f);
s << m_Settings;
}
}
QDataStream posiada operator do zapisu QMap, co czyni kod zapisywania niezwykle łatwym. Odczytywanie jest równie proste:
void SettingsManager::load()
{
QFile f("settings");
if(f.open(IO_ReadOnly))
{
QDataStream s(&f);
s >> m_Settings;
}
}
Dla ułatwienia, ustawienia ładujemy tylko przy uruchomieniu aplikacji. Ustawienia będą zapisane na dysk po zamknięciu okna dialogowego ustawień.
Dostosowanie kodu do KDE
Tym, którzy nie muszą korzystać z takich walorów Qt jak jego właściwość łatwego przenoszenia między platformami i niewielki obszar zajmowanej pamięci, ale chcieliby skorzystać z większej ilości gotowych widżetów i klas użytkowych, powinni rozważyć użycie środowiska pulpitowego K (KDE). KDE oferuje bowiem znacznie więcej rozwiązań niż Qt, jak choćby centralnego menedżera konfiguracji (ang. central configuration manager), skąd można modyfikować wygląd aplikacji. KDE oferuje też analizator składniowy konfiguracji (ang. configuration parser), wiele nowych widżetów (takich jak w pełni rozwinięty analizator składniowy i widżet HTML) oraz klasy pomocnicze, jak na przykład korektor ortograficzny pisowni (ang. spell checker).
Zanim zaczniemy, winni jesteśmy kilka słów ostrzeżenia:
Gdy pisaliśmy tę książkę, KDE 2 nie był jeszcze opublikowany. Kod w tym rozdziale był napisany i testowany jako wersja KDE beta 2. Mamy nadzieję, że między wersją testową beta i wersją końcową nie będzie zbyt dużo niezgodności, które mogłyby mieć znaczący wpływ na przedstawiony tutaj kod.
Zamierzamy w dalszym ciągu trzymać się założeń tej prezentacji i dbać o to aby nasza aplikacja nie odbiegała zanadto od wersji napisanej za pomocą GNOME/GTK+. Z tego też powodu nie zawsze będziemy w pełnej zgodzie z konwencjami, ustalonymi dla „prawdziwych” aplikacji KDE. Z konwencjami dla aplikacji KDE można zapoznać się w poradniku stylu na stronie WWW: http://developer.kde.org/documentation/standards/kde/style/basics/index.html.
Poradnik stylu dotyczy jedynie zagadnień związanych z aspektem dekoracyjnym interfejsu GUI. Techniczne aspekty projektu pozostają bez zmian i na nich będziemy się tutaj koncentrować. W tym podrozdziale przyjrzymy się, jak z naszej aplikacji Qt uczynić aplikację zgodną w większym stopniu z KDE. Omówimy:
obiekt KApplication,
okno główne,
okno dialogowe tytułu (ang. title dialog),
KConfig i SettingsManager
Jak wspomniano w poprzednim rozdziale, pierwszym krokiem na drodze do przekształcenia aplikacji Qt w aplikację KDE jest zastąpienie QApplication z Qt przez KApplication z KDE. Aplikacje KDE 2 musza także posiadać obiekt KAboutData. Ta klasa jest używana do przechowywania takich informacji o aplikacji, jak jej nazwa, opis, prawa autorskie i strona główna (ang. homepage). Powinno być możliwe przekazanie nazwy użytkownika i hasła z wiersza poleceń. KDE udostępnia analizator składniowy wiersza poleceń (KCmdLineArgs). Niemniej jednak w niektórych beta wersjach KDE 2, nie działa on poprawnie (może wysłać ostrzeżenie, wbrew stanu faktycznemu, że dane o aplikacji nie zostały określone) i dlatego nie umieścimy go w naszym kodzie:
#include <kapp.h>
#include <kaboutdata.h>
#include <kcmdlineargs.h>
#include "mainwindow.h"
#include "settingsmanager.h"
int main(int argc, char **argv)
{
KAboutData aboutdata( "dvdstore",
"DVDstore",
"1.0",
"Wrox Press Demo Application",
KAboutData::License_GPL,
"(c) 2000, Wrox Press",
"http://www.wrox.com",
"A KDE GUI for DVDstore conceived in\n"
"'Professional Linux Programming'\n"
"Wrox Press 2000",
"none");
aboutdata.addAuthor("Marius Sundbakken");
KCmdLineArgs::init(argc, argv, &aboutdata);
KApplication app;
...
Przez proste zastąpienie obiektu aplikacji zasadniczo zmieniliśmy wygląd naszej aplikacji. Zmieniły istotnie wygląd: pasek narzędzi, czcionka i większość widżetów:
Rys., str. 501
|
Ten wygląd może być teraz centralnie konfigurowany ośrodka sterowania pulpitem KDE.
Kolejnym krokiem będzie zmiana okna głównego tak, aby było pochodną KTMainWindow, zamiast QMainWindow. Tym sposobem uzuskujemy pełne zarządzanie sesją. Położenie paska narzędzi będzie zapisane wraz z położeniem i geometrią okna.
Użycie KTMainWindow wymaga wprowadzenia kilku innych zmian. Należy używać KMenuBar i KToolBar, zamiast ich ekwiwalentów z Qt. Zastąpienie pliku nagłówkowego qmenubar.h przez kmenubar.h rozwiąże pierwszą ze wspomnianych kwestii. Wymiana paska narzędzi wiązać się będzie z uaktualnieniem mainwindow.h tak, by zawierał ktoolbar.h, oraz wymaga zastąpienia QToolBar przez KToolBar zarówno w pliku nagłówkowym jak i źródłowym:
#include <ktmainwindow.h>
#include <ktoolbar.h>
...
class MainWindow : public KTMainWindow
{
...
private:
KToolBar *m_pToolBar;
...
};
Konstruktor KToolBar różni się nieznacznie od konstruktora QToolBar — większa różnica zachodzi między przyciskami obu pasków narzędzi. Nie zamierzamy poświęcać więcej czasu użyciu QToolButton — metoda konstrukcji naszego paska narzędziowego KDE nie różni się prawie od zastosowanej wcześniej metody konstruowania menu. W pliku nagłówkowym pominiemy więc wszystkie obiekty przycisków narzędzi i zastąpimy je prostymi identyfikatorami całkowitymi ID, tak jak to zrobiliśmy w przypadku paska menu. Do tworzenia przycisków używamy insertbutton z KToolBar.
Dla porównania przedstawimy kod dla obydwu wersji — wyjściowej wersji Qt i nowej wersji KDE.
Najpierw wersja Qt:
m_pConnectButton = new QToolButton(
QPixmap(DVDSTORE_ICON_CONNECT),
"Connect",
"Connect to database",
this,
SLOT(connectDatabase()),
m_pToolBar);
W wersji KDE używamy programu ładującego ikony KDE (ang. KDE icon loader), aby uzyskać obiekt QPixmap. Program ładujący ikony odpowiada za dostarczenie ikon, pasujących do bieżącego wzornictwa pulpitu, wybranego przez użytkownika:
KIconLoader il;
QPixmap pixmap = il.loadIcon("socket", KIcon::Toolbar, 32);
Tutaj "socket" odnosi się do nazwy pliku ikony, ale bez rozszerzenia. Określamy również 1). grupę ikon (pasek narzędzi), do której zawita program ładujący ikony oraz 2). rozmiar w pikselach.
m_pToolBar->insertButton(
pixmap,
m_ConnectButton,
SIGNAL(clicked()),
this,
SLOT(connectDatabase()),
true,
"Connect");
Mapa pikselowa jest taka sama, jak poprzednio.
Kolejną czynnością będzie przekazanie identyfikatora ID przycisku, który zastąpi nasze obiekty QToolButton.
Bardziej interesujący jest następny parametr, określający sygnał, którego będziemy nasłuchiwać. Nie podawaliśmy tego w wersji Qt — Qt automatycznie używał sygnału clicked (kliknięty). W tym przypadku również zamierzamy użyć sygnału clicked (tylko ten sygnał ma sens w tej sytuacji).
Dwa następne parametry określają obiekt i szczelinę. Następnie uaktywniamy przycisk i nadajemy mu etykietę.
To jest bardzo „surowa” metoda tworzenia przycisku paska narzędzi. W rzeczywistości KDE udostępnia klasę KAction ułatwiającą cały proces i jednocześnie zapewniającą zgodność między odpowiadającymi sobie elementami menu i paska narzędzi. Nie będziemy jednak omawiać KAction tutaj.
Zachodzi też potrzeba zmiany kilku wywołań funkcji:
setCentralWidget jest zmieniony na setView,
setUsesTextLabel jest zastąpiony przez setIconText,
i addSeparator jest wymieniony na insertSeparator.
Po tych modyfikacjach paska narzędzi, widok naszej aplikacji ponownie uległ zmianie. I na koniec, ponieważ nie używamy już obiektów QToolButton, ale identyfikatorów całkowitych, będziemy musieli zmienić kod tak, aby uaktywniał i wyłączał działanie przycisków paska narzędzi:
m_pToolBar->setItemEnabled(m_DisconnectButton, false);
Rys., str. 503.
|
Teraz przekształcimy jedno z naszych okien dialogowych Qt w okno dialogowe zgodne z KDE. Do przekształceń wybraliśmy okno dialogowe tytułu. Będziemy więc mogli zamiast wykorzystywać go jako prosty edytor wiersza przeznaczony dla użytkownika do wprowadzenia daty, użyć terminarz KDE (ang. KDE's date picker). Klasą podstawową okien dialogowych KDE jest KDialogBase. Dostarcza ona definiowane wstępnie układy (ang. layouts) i standardowe przyciski, takie jak OK (Zatwierdź), czy Cancel (Anuluj). Na początek plik nagłówkowy:
#include <kdatepik.h>
#include <kdialogbase.h>
...
class TitleDialog : QKDialogBase
{
...
Mamy tutaj szczelinę okay. Skoro jednak mamy zamiar usunąć nasz własny przycisk OK i użyć przycisku wstępnie zdefiniowanego, powinniśmy użyć chronionej szczeliny wirtualnej slotOK z KDialogBase:
protected slots:
void slotOk();
private:
KDatePicker *m_pReleaseDate;
...
};
W titledialog.cpp:
TitleDialog::TitleDialog(int title_id, QWidget *parent, const char
*name):
KQDialogBase(parent, name, true)
{
showButtonOK(true);
showButtonApply(false);
showButtonCancel(true);
Te trzy wiersze wystarczą do utworzenia przycisków OK i Cancel. Przycisk Apply (Zastosuj) nie będzie nam potrzebny. Klasa KDialogBase zawiera pewną liczbę wstępnie zdefiniowanych układów. Niestety żaden z nich nie odpowiada naszym potrzebom. Tworzymy więc wypełniacz widżetu (ang. widget placeholder) dla naszego własnego układu i przekazujemy ten widżet do KDialogBase:
QWidget *thispage = new QWidget(this);
setMainWidget(thispage);
Chcemy również użyć proponowane przez KDE odstępy między widżetami. Reszta kodu odnosząca się do this musi być zatem uaktualniona i zamiast tego pojawić musi się thispage:
QVBoxLayout *main = new QVBoxLayout(thispage, 4, spacingHint());
...
Terminarz KDE:
...
m_pReleaseDate = new KDatePicker(topgrid);
...
}
Nasza nowa szczelina:
void TitleDialog::slotOk()
{
...
Qdate d = m_pReleaseDate->getDate();
Teraz musimy zbudować łańcuch daty, który należy przekazać do biblioteki bazy danych. Zazwyczaj w tym celu wykorzystuje się funkcję QString::arg. My jednak użyjemy QString::sprintf, gdyż potrzebny nam jest format YYYYMMDD:
QString str;
str.sprintf("%04d%02d%02d", d.year(), d.month(), d.day());
strcpy(new_title.release_date, str);
...
}
ak można zobaczyć poniżej na zrzucie ekranu, duży widżet terminarza zastąpił malutkie, jednowierszowe pole edycji. Jednakże interfejs GUI jest wciąż doskonale wyrównany i uporządkowany. Nie byłoby to możliwe, gdybyśmy rozmieścili widżety w ustalonych współrzędnych. Rzeczywiste rozmieszczenie jest jednak niezależne od nas, ponieważ korzystamy z układów (ang. layouts). Zostaje nam do dyspozycji wielka dowolność dodawania i usuwania widżetów, niezależnie od ich rozmiarów. Zobaczymy też na rysunku nowe przyciski OK i Cancel:
Rys., str. 505
|
Kconfig i SettingsManager
Qt nie posiada analizatora konfiguracji pliku, toteż napisaliśmy nasz własny. KDE, zaś, zapewnia klasę KConfig. Jest znacznie bardziej skomplikowana niż nasza klasa menedżera ustawień, ale pracuje podobnie, poprzez określanie kluczy i wartości. Obiekt Kconfig można uzyskać z KApplication::sessionConfig, równoważnej naszej funkcji SettingsManager::instance.
Jak mogliśmy się przekonać, używanie klas KDE jest równie łatwe jak posługiwanie się klasami Qt. KDE wykorzystuje w przybliżeniu te same konwencje nazewnictwa, co Qt, zatem stosowanie równoczesne KDE i Qt nie pozbawia kodu jasnego, czystego i spójnego wyglądu. Moglibyśmy, zastępując każdy fragment naszej aplikacji Qt ekwiwalentem z KDE, kontynuować przekształcanie. Niestety jednak, ani czasu, ani miejsca nie mamy w nadmiarze. Pozostawiamy więc Czytelnikowi kontynuacje naszego eksperymentu, wraz z pełnym zapisem kodu, który może być pobrany z witryny WWW wydawnictwa Wrox: www.wrox.com.
Materiały źródłowe
Gamma et al.; Design Patterns: Elements of Reusable Object-Oriented Software. Wyd. Addison Wesley (ISBN 0-201-63361-2).
Martin et al.; Pattern Languages of Program Design 3. Wyd. Addison Wesley (ISBN 0-201-31011-2).
Witryna WWW Trolltech: http://www.trolltech.com/
Archiwum listy korespondencyjnej, zainteresowanych Qt: http://qt-interest.trolltech.com/
Wiadomości o KDE, jak również kod źródłowy KDE: http://www.kde.org/
Archiwum list korespondencyjnych KDE: http://lists.kde.org/
Podsumowanie
W tym rozdziale przyjrzeliśmy się tworzeniu graficznego interfejsu użytkownika (GUI) przy użyciu Qt. Następnie dostosowaliśmy go częściowo do środowiska pulpitowego K (KDE). Mimo, że nie wszystkie komponenty Qt zostały przedyskutowane podczas opisu tworzenia GUI, te najbardziej podstawowe nie zostały pominięte. Zastępowanie użytych przez nas widżetów i okien dialogowych przez inne, nie powinno stanowić dla nikogo problemu. W wystarczającym stopniu omówiliśmy także sygnały i szczeliny, by mogły być sprawnie wykorzystane. GUI pozbawiony możliwości zastosowania jest bezwartościowy. Zademonstrowaliśmy więc, jak praktycznie użyć elementów GUI wraz z biblioteką języka C, by dotrzeć do bazy danych Filmoteki DVD (DVD Store). Zakończyliśmy rozdział przystosowując aplikację, napisaną wyłącznie za pomocą Qt, do użycia w środowisku KDE.
Zachęcamy Czytelnika do wszechstronnego wykorzystania rewelacyjnej dokumentacji referencyjnej Qt. Jeśli nie okaże się ona wystarczającym źródłem informacji, proponujemy przeszukanie archiwum listy korespondencyjnej dotyczącej Qt: http://qt-interest.trolltech.com/. Niezależnie od okoliczności, gorąco polecamy przyłączenie się do tej listy ze stosunkowo niewielką objętością i doskonałą zawartością. Pomocą w programowaniu dla KDE też mogą służyć listy korespondencyjne, jak na przykład: http://www.kde.org/contact.html. By odwiedzić archiwa, należy zajrzeć na stronę WWW: http://lists.kde.org.
Rysunek pingwina na plaży. Str. 508.
Dyskusja online: http://www.p2p.wrox.com
2 Część I ♦ Podstawy obsługi systemu WhizBang (Nagłówek strony)
2 C:\Robert\Helion\plp14_d.doc
Okno Główne
Rozłącz
Dodaj Tytuł
Połącz
Klient
Ustawienia
Szukaj
Tytuł
Wypożyczenia
Rezerwacje
Zwrot
Raport
Szukaj tytuł
Szukaj klienta
Szukaj płyty
Baza danych
Usuń klienta
Usuń tytuł
Dodaj klienta
Dodaj tytuł