Adresy (Qt5) ################### Niniejszy scenariusz pokazuje, jak zacząć programowanie z wykorzystaniem biblioteki Qt w wersji 5 przy użyciu dedykowanego środowiska IDE Qt Creator. Celem jest stworzenie prostej 1-okienkowej książki adresowej, w której można dodawać dane adresowe powiązane z określoną nazwą, np. imieniem i nazwiskiem. .. contents:: :depth: 1 :local: Nowy projekt ***************** Po uruchomieniu aplikacji Qt Creator wybieramy przycisk "New Project", który uruchamia kreatora aplikacji. W pierwszym oknie "Applications" i "Qt Widget Applications", co oznacza, że chcemy utworzyć program z interfejsem graficznym oparty na klasie QWidget. W następnym oknie podajemy nazwę projektu, np, "adresy", oraz wskazujemy ścieżkę do katalogu, w którym będą zapisywane pliki wchodzące w skład projektu. W następnym oknie wybieramy tzw. "kit", czyli zestaw definiujący docelowe środowisko, kompilator itp. ustawienia. Dostępne zestawy muszą być wcześniej określone w ustawieniach Qt Creatora Kolejne okno pozwala definiować nazwę klasy głównej i klasę podstawową, podajemy "adresy" i wybieramy "QWidget". W następnym ostatnim oknie niczego nie zmieniamy, kończymy kliknięciem przycisku "Finish". Efektem działania kreatora będzie utworzenie następujących plików: 1) ``adresy.h`` - :term:`plik nagłówkowy`, tutaj będziemy deklarować wszystkie używane w programie obiekty (elementy interfejsu), a także publiczne sloty, czyli funkcje powiązanie z określonymi sygnałami (zdarzeniami). 2) ``adresy.cpp`` - :term:`plik źródłowy`, tu znajdzie się kod tworzący obiekty interfejsu, łączący sygnały ze slotami, a wreszcie implementacja slotów. 3) ``main.cpp`` - plik źródłowy, w którym tworzona i uruchamiana jest instancja naszej aplikacji. 4) ``adresy.ui`` - jak wskazuje rozszerzenie ("ui" - ang. user interface), plik zawierać będzie opis graficznego interfejsu aplikacji zapisany za pomocą znaczników XML. Tworzenie interfejsu ********************** Zaczniemy od utworzenia głównego okna naszej aplikacji. W tym celu dwa razy klikamy plik adresy.ui i przechodzimy do tworzenia formularza. Na początku klikamy obiekt "Grid Layout" z kategorii "Layouts" i rysujemy prostokąt na formularzu tak, aby nie wypełniał go w całości. Dodana kontrolka umożliwia porządkowanie innych elementów tworzących interfejs w prostokątnej siatce. Dalej dodamy dwie etykiety, czyli obiekty "Label" z kategorii "Display Widgets". Staramy się je umieścić jedna nad drugą w dodanej przed chwilą siatce. .. tip:: Po wybraniu obiektu i najechaniu na *Grid Layout* należy obserwować niebieskie podświetlenia, które pojawiają się w pionie i poziomie, wskazują one, gdzie umieszczony zostanie dodawany obiekt. Po dwukrotnym kliknięciu na dodane etykiety możemy zmienić treść przez nie wyświetlaną. Modyfikujemy w ten sposób właściwość *text* danego obiektu. Etykieta górna powinna zawierać tekst "Nazwa", dolna - "Adresy". .. note:: Lista wszystkich obiektów wyświetlana jest po prawej stronie na górze w oknie *Hierarchia obiektów*. W kolumnie *Obiekt* widzimy tam nazwy dodanych obiektów, a w kolumnie *Klasa* nazwy klas, które reprezentują. Po wskazaniu myszą dowolnego obiektu możemy edytować wszystkie jego właściwości poniżej. Np. nazwę obiektu zmienimy w polu *objectName*. Nazwę etykiety górnej ustalamy na "nazwaLbl", dolnej - na "adresyLbl". .. tip:: Konwencji nazywania obiektów jest wiele, ważne żeby konsekwentnie trzymać się wybranej. Tutaj proponujemy uwzględnianie w nazwie typu obiektu przy użyciu skrótu pisanego z dużej litery, np. "nazwaLbl". Po prawej stronie etykiety "Nazwa" dodajemy kontrolkę *Line Edit* z grupy *Input Widgets* o nazwie "nazwaLine". Poniżej, czyli w drugiej kolumnie, tworzymy obiekt *Text Edit* z tej samej grupy, co poprzedni o nazwie "adresText". Powinniśmy uzyskać poniższy układ: .. figure:: img/qt5_08.png Czas na dodanie przycisków pozwalających inicjować działanie aplikacji. Dodajemy więc 5 przycisków *PushButton* z kategorii *Buttons* po prawej stronie i poza(!) obiektem *GridLayouts* jeden pod drugim. Na samym dole umieszczamy kontrolkę *Vertical Spacer* z kategorii *Spacers*. Następnie zaznaczamy wszystkie dodane obiekty, obrysowując je myszką, i klikamy ikonę *Rzmieść w pionie* (:kbd:`CTRL+L`) na pasku narzędziowym. Teraz stworzoną grupę przeciągamy na siatkę jako 3. kolumnę. Musimy zmienić nazwy i tekst dodanych przycisków. Od góry ustawiamy kolejne właściwości (nazwa/tekst): "dodajBtn/Dodaj", "zapiszBtn/Zapisz", "anulujBtn/Anuluj", "edytujBtn/Edytuj", "usunBtn/Usuń". W efekcie powinniśmy uzyskać następującą formatkę: .. figure:: img/qt5_09.png Musimy dodać jeszcze 3 przyciski pozwalające na nawigację między adresami i wyjście z programu. Poniżej obiektu siatki umieszczamy więc 2 przyciski (*PushButton*), zaznaczamy je i klikamy ikonę *Rozmieść poziomo w splitterze*, następnie przeciągamy grupę na dół 2. kolumny siatki. Na koniec dodajemy jeszcze jeden przycisk na dole 3. kolumny. Dodanym obiektom zmieniamy właściwości (nazwa/tekst): "poprzBtn/Porzedni", "nastBtn/Następny", "koniecBtn/Koniec". Na koniec zaznaczamy formularz główny, na którym znajdują się wszystkie elementy interfejsu i klikamy przycisk *Rozmieść w siatce* (:kbd:`CTRL+G`). Dzięki temu kontrolki będą skalowane wraz ze zmianą rozmiaru okna. W sumie uzyskujemy poniższy projekt: .. figure:: img/qt5_10.png Możemy uruchomić naszą aplikację, wybierając *Budowanie/Uruchom* (:kbd:`CTRL+R`) lub klikając trzecią od dołu ikonę zielonego trójkąta w lewej kolumnie Qt Creatora. Powinniśmy zobaczyć podobne do poniższego okno: .. figure:: img/qt5_11.png Deklaracje i implementacje ************************** Po dodaniu elementów interfejsu musimy zadeklarować zmienne, za pomocą których będziemy mogli nimi manipulować. Przechodzimy do pliku ``adresy.h`` i wprowadzamy poniższe zmiany: .. raw:: html
adresy.h nr
.. highlight:: cpp .. literalinclude:: adresy01.h :linenos: Na początku musimy zaimportować klasy, z których skorzystaliśmy przy budowie interfejsu. Najważniejszą jest klasa podstawowa wszystkich elementów interfejsu, czyli ``QWidget``. Kolejne trzy odpowiadają wykorzystanym przez nas kontrolkom edycyjnym i przyciskom. Dodatkowa klasa ``QTextCodec`` pozwoli poprawnie wyświetlać polskie znaki. W wewnątrz naszej klasy głównej, której deklaracja rozpoczyna się w linii 14., deklarujemy prywatne (:term:`private`) właściwości, których nazwy odpowiadają nazwom wcześniej dodanych elementów interfejsu graficznego. Formalnie każda zmienna jest wskaźnikiem do obiektu odpowiedniego typu. W pliku ``adresy.cpp`` korzystamy ze zadekarowanych zmiennych, aby ustawić początkowe właściwości obiektów składających się na interfejs użytkownika. .. raw:: html
adresy.cpp nr
.. highlight:: cpp .. literalinclude:: adresy01.cpp :linenos: W obrębie konstruktora głównej klasy naszej aplikacji o nazwie ``adresy``, którego definicja rozpoczyna się w linii 4., tworzymy instancje klas użytych w interfejsie graficznym. Do zmiennych zadeklarownych w pliku ``adresy.h`` przypisujemy obiekty utworzone za pomocą operatora ``new``, a następnie definiujemy ich początkowe właściwości. Konstruktorowi odpowiada zawzwyczaj destruktur, a więc działanie, które usuwa stworzony obiekt, w tym wypadku interfejs użytkownika: ``adresy::~adresy()``. Aby określić stan elementów interfejsu wykorzystujemy odpowiednie właściwości i metody reprezentujących je obiektów. Np. właściwość ``setReadOnly(true)`` blokuje edycję danego elementu, a właściwość ``setEnabled(false)`` uniemożliwia kliknięcie danego przycisku. Metoda ``hide()`` ukrywa obiekt. Instrukcja ``QTextCodec::setCodecForLocale(QTextCodec::codecForName("UTF-8"))`` określa kodowanie komunikatów w standardzie "UTF-8" używanych w aplikacji, które wprowadzane są dalej za pomocą funkcji ``trUtf8()``. Tak dzieje się np. podczas określania tytułu okna w wywołaniu ``setWindowTitle()``. .. tip:: W środowisku MS Windows kodowanie powinno zostać ustawione na ``Windows-1250``. Dzięki powyższym uzupełnieniom po uruchomieniu aplikacji pola nazwy i adresu będą nieaktywne, będziemy mogli natomiast użyć przycisków ``Dodaj``, aby utworzyć nowy wpis, lub ``Koniec``, aby zakończyć aplikację. .. figure:: img/qt5_12.png Sygnały i sloty ************************** Działanie aplikacji z interfejsem graficznym polega w uproszczeniu na reagowaniu na działania użytkownika, takie jak np. kliknięcie, naciśnięcie klawisza, przeciągnięcie itp. Wszystkie **zdarzenia** generowane z poziomu interfejsu użytkownika w terminologii biblioteki :term:`Qt` emitują tzw. :term:`sygnały`. Programista decyduje o tym, które z nich i jak są obsługiwane, definiując tzw. :term:`sloty`, czyli **funkcje** powiązane z określonymi zdarzeniami. Mechanizm sygnałów i slotów umożliwia komunikację między obiektami aplikacji. Każda z funkcji obsługujących zdarzenia musi zostać najpierw zadeklarowana w pliku ``adresy.h`` w sekcji ``public slots:``, ich implementację musimy dopisać później do pliku ``adresy.cpp``. .. raw:: html
adresy.h nr
.. highlight:: cpp .. literalinclude:: adresy02.h :emphasize-lines: 5,7-9,24-28 :lineno-start: 18 :lines: 18- Oprócz deklaracji `slotów` w liniach 24-26 dopisujemy deklaracje kilku potrzebnych zmiennych. Definiujemy więc typ wyliczeniowy ``Tryb``, z którego korzystamy deklarując zmienną ``aktTryb`` oraz prywatną funkcję pomocniczą ``aktGui``. Posłużą one do określania 1 z 3 stanów działania aplikacji, takich jak: przeglądanie wpisów, dodawanie i ich edycja. Dalej dopisujemy deklaracje zmiennych pomocniczych ``staraNazwa`` i ``staryAdres``. Korzystamy tu z typu ``QString`` oznaczającego dane tekstowe. Na końcu deklarujemy specjalną zmienną ``kontakty``, która posłuży do przechowywania nazw i skojarzonych z nimi adresów w postaci słownika typu ``QMap``. Poszczególne elementy takiej listy mają postać skojarzonych ze sobą par ``(klucz, wartość)``. .. raw:: html
adresy.cpp nr
.. highlight:: cpp .. literalinclude:: adresy02.cpp :emphasize-lines: 1-2 :lineno-start: 53 :lines: 53- Powiązania między sygnałami i slotami ustalamy w pliku ``adresy.cpp`` za pomocą poleceń typu: ``connect(dodajBtn, SIGNAL(clicked()), this, SLOT(dodajKontakt()));``. Funkcja ``conect()`` jako pierwszy argument wymaga zmiennej wskazującej obiekt, który emituje sygnał określony w 2. argumencie (np. ``SIGNAL(clicked())``, czyli kliknięcie), 3. argument określa obiekt, który zostaje *powiadomiony* o zdarzeniu, w ostatnim argumencie podajemy funkcję, która ma zostać wykonana (``SLOT(dodajKontakt())``). Jak widać powyżej, na końcu konstruktora naszej klasy ``adresy`` wiążemy kliknięcia przycisków ``dodajBtn`` i ``koniecBtn`` z funkcjami ``dodajKontakt()`` i ``koniec()``. Funkcja ``dodajKontakt()`` przygotowuje aplikację do przełączenia w stan dodawania nowych danych. W tym celu najpierw zapamiętujemy dotychczasową nazwę i adres, a następnie wywołujemy funkcję pomocniczą z argumentem typu ``Tryb`` oznaczającym wymagany stan aplikacji: ``aktGui(dodajT)``. Działanie funkcji ``aktGui()``, obsługującej stany aplikacji, polega na uaktywnianiu lub wyłączaniu określonych elementów interfejsu w zależności od przeprowadzanej przez użytkownika czynności. Np. w trybie dodawania i edycji odblokowujemy możliwość wprowadzania tekstu w polach nazwy (``nazwaLine->setReadOnly(false);``) i adresu (``adresText->setReadOnly(false);``), pokazujemy przyciski pozwlające na zapis lub anulowanie wywołując metodę ``show()``. Wyłączamy również nawigację, blokując odpowiednie przyciski (metoda ``setEnabled(false)``). Po wejściu w tryb nawigacji czyścimy (``clear()``) zawartość pól nazwy i adresu, o ile lista kontaktów jest pusta (``if (kontakty.isEmpty())``). Następnie uaktywniamy przyciski edycji, usuwania i przeglądania, jeżeli mamy jakieś kontakty. Ilość kontaktów zapisujemy wcześniej w osobnej zmiennej (``int ile=kontakty.size();``). Na koniec przyciski zapisu i anulowania zostają zablokowane. Slot ``koniec()`` wywoływany jest po kliknięciu przycisku *Koniec* i powoduje zamknięcie aplikacji przy użyciu metody ``close()``. Wywołuje ona m.in. destruktor klasy, co powoduje – w naszym przypadku – usunięcie instancji obiektu interfejsu graficznego (``delete ui;``). Dodawanie adresów ************************** Pora zaimplementować obsługę trybu dodawania danych adresowych. Najpierw do pliku nagłówkowego dopisujemy deklaracje odpowiednich slotów: .. raw:: html
adresy.h nr
.. highlight:: cpp .. literalinclude:: adresy03.h :emphasize-lines: 4-5 :lineno-start: 25 :lines: 25-29 Musimy też na początku pliku dodać import klasy ``QMessageBox`` pozwalającej wyświetlać informacje użytkownikowi. Następnie przechodzimy do pliku ``adresy.cpp``, w którym trzeba powiązać sloty ``zapiszKontakt()`` i ``anuluj()`` ze zdarzeniem kliknięcia przycisków ``zapiszBtn`` i ``anulujBtn``. Zadanie to proponujemy wykonać samodzielnie :-). Na końcu pliku musimy dopisać definicje powiązanych funkcji: .. raw:: html
adresy.cpp nr
.. highlight:: cpp .. literalinclude:: adresy03.cpp :lineno-start: 118 :lines: 118- Funkcja ``zapiszKontakt()`` pobiera tekst wpisany w pola edycyjne za pomocą metod ``text()`` oraz ``toPlainText()`` i zapisuje je w zmiennych tekstowych. Następnie sprawdza, czy użytkownik wprowadził obydwie informacje. Jeżeli nie, wyświetla odpowiedni komunikat przy użyciu metody ``QMessageBox::information()``. Pierwszy tekst, który przekazujemy do tej funkcji to tytuł okna dialogowego, drugi – właściwy komunikat. Następnie, jeżeli aplikacja jest w trybie dodawania, sprawdza, czy podana nazwa nie została zapisana wcześniej na liście ``kontakty``. Jeśli nie (``if (!kontakty.contains(nazwa))``), dodaje nowe dane (``kontakty.insert(nazwa, adres);``) i wyświetla potwierdzenie. W przeciwnym razie informuje użytkownika o duplikacie. Na końcu aktywuje tryb nawigacji (``aktGui(nawigujT);``). Jeżeli użytkownik rozmyśli się i kliknie odpowiedni przycisk, wywoływana jest funkcja ``anuluj()``. Jak widać, przywraca ona w polach edycyjnych poprzednio wprowadzane dane i również aktywuje tryb nawigacji. Tryb nawigacji ******************** Obsługa nawigacji wymaga napisania funkcji obsługujących naciśnięcie przycisków *Następny* i *Poprzedni*, które stają się aktywne, jeżeli mamy więcej niż 1 dodany adres. Jak zwykle, zaczynamy od zadeklarowania publicznych slotów ``nast()`` i ``poprz()`` w pliku nagłówkowym. Dopisanie tych 2 linijek pozostawiamy do samodzielnego wykonania. Podobnie powiązanie zadeklarowanych slotów z sygnałami (kliknięciami) obiektów ``nastBtn`` i ``poprzBtn`` w konstruktorze klasy ``adresy``. Następnie dopisujemy implementację zadeklarowanych funkcji na końcu pliku ``adresy.cpp``: Na końcu pliku musimy dopisać definicje powiązanych funkcji: .. raw:: html
adresy.cpp nr
.. highlight:: cpp .. literalinclude:: adresy04.cpp :lineno-start: 149 :lines: 149- Wyświetlając kolejną parę powiązanych danych, tzn. nazwę i przypisany jej adres(y), musimy sprawdzić w fukcji ``nast()``, czy mamy kolejny wpis, czy też aktualny jest ostatni. Wtedy należałoby wyświetlić wpis pierwszy. W tym celu pobieramy nazwę aktualnie wyświetlonego wpisu i tworzymy obiekt tzw. ``iteratora`` inicjowanego przez metodę ``find()`` i przypisanego do zmiennej ``i``: ``QMap::iterator i = kontakty.find(nazwa);``. Iterator umożliwia łatwe poruszanie się po liście słowników zapisanych w zmiennej ``kontakty``. Metoda ``i.key()`` zwraca nam klucz, a ``i.value()`` przypisaną mu wartość. Jeżeli bieżący wpis nie jest ostatnim inkrementujemy wartość iteratora (``if (i != kontakty.end()) i++;``). W przeciwnym wypadku ustawiamy go na pierwszy wpis (``i = kontakty.begin();``); Na koniec pozostaje wczytanie nazwy (``i.key()``) i przypisanych jej danych (``i.value()``) do odpowiednich pól interfejsu. Funkcja ``poprz()`` zaczyna się tak samo jak poprzednia, czyli od utworzenia iteratora wskazującego na bieżący wpis. Jeżeli jesteśmy na początku listy, ustawiamy iterator na element końcowy. Następnie przechodzimy do elementu końcowego (``i--``) i wyświetlamy odpowiednie dane. .. note:: Metoda ``.end()`` klasy ``QMap`` zwraca iterator wskazujący na wirtualny (!) element po ostatnim elemencie listy. Dlatego, aby uzyskać dostęp do niego, musimy iterator dekrementować (``i--``). Edycja i usuwanie ******************** Do oprogramowania zostay jeszcze dwa przyciski: ``btnEdytuj``, którego kliknięcie powinno wywołać funkcję ``edytujKontakt()``, oraz ``btnUsun``, który wywołuje funkcję ``usunKontakt()``. Samodzielnie dopisujemy deklaracje funkcji do pliku nagłówkowego, a ich powiązania z sygnałami umieszczamy w pliku źródłowym. Następnie implementujemy funkcje: .. raw:: html
adresy.cpp nr
.. highlight:: cpp .. literalinclude:: adresy05.cpp :lineno-start: 185 :lines: 185- Przejście do trybu edycji, czyli działanie funkcji ``edytujKontak()``, polega na zapisaniu aktualnie wyświetlanych danych (przydatne, jeżeli użytkownik anuluje zmiany) i uaktywnieniu trybu (``aktGui(edytujT);``), tzn. odblokowaniu pól tekstowych i odpowiednich przycisków. Usuwanie kontaktów również jest proste. Na początku pobieramy *nazwę* i związany z nim *adres(y)*. Metoda ``.contains(nazwa)`` pozwala sprawdzić, czy lista kontaktów zawiera słownik o podanym kluczu. Natępnie prosimy użytkownika o potwierdzenie operacji. Po jego uzyskaniu najpierw wyświetlamy w aplikacji dane poprzedniego wpisu dzięki wywołaniu zdefiniowanej wcześniej funkcji ``poprz()``, później dopiero usuwamy wpis za pomocą metody ``.remove(nazwa)`` i wyświetlamy potwierdzenie. Na koniec aktywujemy tryb nawigacji. Poćwicz sam ============== Spróbuj rozszerzyć napisaną aplikację o możliwość przechowywania danych w pliku lub w bazie na dysku. Materiały ************* 1. `Projekt Qt`_ 2. `Biblioteka Qt 5`_ 3. `Qt Creator`_ 4. `Dokumentacja Qt 5`_ 5. `Qt Developer Wiki (pl)`_ .. _Projekt Qt: https://qt-project.org/ .. _Biblioteka Qt 5: http://doc.qt.io/qt-5/ .. _Qt Creator: http://pl.wikipedia.org/wiki/Qt_Creator .. _Dokumentacja Qt 5: http://doc.qt.io/qt-5/reference-overview.html .. _Qt Developer Wiki (pl): http://qt-project.org/wiki/Wiki_Home_Polish