Kursy • Poradniki • Inspirujące DIY • Forum
W pierwszej części tego kursu Qt zbudowaliśmy interfejs z trzema przyciskami, pamiętacie? A teraz, bez zagłębiania się w szczegóły, wykonamy podobny interfejs, ale tym razem za pomocą nowej technologii od Qt, czyli Qt Quick i QML. Zaczynamy od razu od przykładu, a później wrócimy do omówienia tych mechanizmów. Standardowo tworzymy nowy projekt, wybierając opcję Qt Quick Application - Empty:
W rezultacie powinniśmy znaleźć się w trybie edycji pliku main.qml o mniej więcej takiej zawartości:
1 2 3 4 5 6 7 8 9 |
import QtQuick 2.12 import QtQuick.Window 2.12 Window { visible: true width: 640 height: 480 title: qsTr("Hello World") } |
Podczas pisania tego poradnika korzystano z Qt 5.12.5 i Qt Creatora 4.10.2 – przykładowy zapis import QtQuick 2.12 oznacza, że importujemy konkretną wersję modułu Qt Quick (tu 2.12).
Jeśli korzystacie ze starszej wersji Qt, prawdopodobnie będziecie musieli zastosować starsze wersje modułów, np. 2.5. Skorzystajcie wtedy z podpowiadania składni, które wskaże aktualnie dostępne wersje. Gdy będziecie chcieli użyć niedostępnej wersji, zobaczycie w konsoli następującą informację:
1 2 |
QQmlApplicationEngine failed to load component qrc:/main.qml:2 module "QtQuick.Window" version 2.13 is not installed |
Dodajmy teraz do aplikacji przycisk, który zostanie umieszczony centralnie pośrodku okna i będzie posiadał podświetlony napis Close. Po jego kliknięciu w konsoli zostanie wypisana informacja o tym fakcie, a po upływie 2 sekund aplikacja zamknie się automatycznie.
Zadanie to można zrealizować za pomocą stosunkowo prostego programu:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
import QtQuick 2.12 import QtQuick.Window 2.12 import QtQuick.Controls 2.12 Window { visible: true width: 640 height: 480 title: qsTr("Hello World") Button { text: "Close" highlighted: true anchors.centerIn: parent onClicked: { timer.start() console.log("Button was clicked") } } Timer { id: timer interval: 2000 // podawany w ms onTriggered: Qt.quit() } } |
Czytając kod, powinniście bez żadnego problemu domyślić się, za co odpowiadają konkretne fragmenty. Uruchomcie program, aby sprawdzić, czy działa zgodnie z oczekiwaniami.
Czym jest QML oraz Qt Quick?
Po takim praktycznym wstępie warto wyjaśnić, czym tak właściwie jest QML i Qt Quick. Dobrym startem będzie lektura dokumentacji Qt. Fragment na temat QML:
QML is a declarative language that allows user interfaces to be described in terms of their visual components and how they interact and relate with one another. It is a highly readable language that was designed to enable components to be interconnected in a dynamic manner, and it allows components to be easily reused and customized within a user interface. Using the QtQuick module, designers (…) have the option of connecting these user interfaces to any back-end C++ libraries.
Fragment na temat Qt Quick:
Qt Quick is the standard library of types and functionality for QML. It includes visual types, interactive types, animations, models and views, particle effects and shader effects. A QML application developer can get access to all of that functionality with a single import statement.
Według mnie najistotniejsze zalety QML to ogromna łatwość i szybkość tworzenia nowoczesnych, ładnie prezentujących się interfejsów graficznych oraz możliwość połączenia go z dowolnymi klasami lub bibliotekami napisanymi w C++.
QML daje nam zatem możliwość podzielenia projektu na dwie części: backendową i frontendową. Zespół projektantów może zająć się w tym samym czasie UI/UX, a zespół deweloperów – logiką biznesową aplikacji.
Podobnej sytuacji nie mogliśmy uzyskać podczas korzystania z Qt Widgets, gdzie interfejs i logika były implementowane w tym samym miejscu. QML daje nam możliwości, które wspólnie tworzą HTML, CSS i JavaScript. Tak, QML ma dużo wspólnego z JavaScriptem, np. cały silnik do jego uruchamiania. Co więcej, QML posiada m.in. takie mechanizmy jak Garbage Collector czy Just-in-time compilation.
To, co można osiągnąć dzięki Qt i QML, świetnie pokazują wpisy na stronie Qt z serii Built with Qt.
Wróćmy do przykładu i ważnych elementów QML
Teraz, gdy mamy już podstawową wiedzę na temat wprowadzonych tu rozwiązań, możemy przejść do omówienia prostego przykładu z początku artykułu.
Hierarchia elementów w QML
Program napisany w QML tworzy hierarchię elementów – każdy zdefiniowany obiekt może stworzyć dowolną liczbę pochodnych obiektów (child objects), dla których staje się rodzicem (parent). W naszym przykładzie nadrzędnym obiektem jest typ Window. Tworzymy w nim dwa pochodne obiekty typu Timer i Button (aby użyć tego typu, musieliśmy zaimportować moduł QtQuick.Controls 2.12).
Qt dla „klas” QML stosuje nazewnictwo „Type”, np. Window QML Type. W kontekście C++ będziemy mówić o klasach, natomiast w przypadku QML – o typach.
Właściwości obiektów w QML
Przyjrzymy się zawartości obiektu Button – text i highlighted to tzw. właściwości (properties). Możemy je porównać do zmiennych definiowanych wewnątrz klasy napisanej w C++, reprezentujących stan wewnętrzny obiektu tej klasy. W przypadku Button za pomocą text ustawiliśmy napis, który wyświetlany jest na przycisku, natomiast za pomocą highlighted podświetliliśmy nasz przycisk. Wszystkie dostępne właściwości tych typów znajdziemy w dokumentacji: Timer oraz Button.
1 2 3 4 5 |
Button { text: "Close" highlighted: true // ... } |
Anchors, czyli pozycjonowanie w QML
Kolejną właściwością, ale nieco bardziej złożoną, jest anchors, czyli mechanizm geometrycznego łączenia obiektów – niezwykle ważna właściwość w QML. W przykładzie umieściliśmy przycisk w środku rodzica – czyli w środku Window. Po więcej informacji odsyłam do dokumentacji.
Mechanizm sygnałów i slotów w QML
Obsługa slotów w QML jest bardzo prosta. Sprawdzamy w dokumentacji, jakie zdefiniowane sygnały ma typ Button – w naszym przykładzie użyliśmy sygnału clicked(), jego obsługę definiujemy za pomocą połączenia on + NazwaSygnału, czyli u nas onClicked. Wielka litera w nazwie slotu jest bardzo istotna! W tym przykładzie w slocie uruchomiliśmy zegar i wypisaliśmy w konsoli informację o naciśnięciu przycisku:
1 2 3 4 5 6 7 |
Button { // ... onClicked: { timer.start() console.log("Button was clicked") } } |
W QML możemy również używać wbudowanych elementów, które daje nam JavaScript. Przykładem może być zastosowany tutaj obiekt konsoli.
Wyrażenia JavaScript w QML
Zauważcie, że oba typy, Button i Timer, obsługują sloty onClicked oraz onTriggered. Obsługa w jednym z nich jest zawarta między nawiasami klamrowymi, a w drugim bezpośrednio po dwukropku:
1 2 3 4 5 6 7 8 9 10 11 12 |
Button { //... onClicked: { timer.start() console.log("Button was clicked") } } Timer { // ... onTriggered: Qt.quit() } |
Obie formy są równoznaczne i poprawne. Nawiasy stosujemy, gdy chcemy wykonać pewien określony blok kodu, ale możemy ich również użyć przy pojedynczych wywołaniach. Poniżej znajdziecie (prawie) wszystkie przykłady, w jaki sposób można to zrobić:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
Timer { // ... onTriggered: { Qt.quit() } } Timer { // ... onTriggered: Qt.quit() } Timer { // ... onTriggered: { Qt.quit() } } |
Poniższy zapis jest także poprawny:
1 2 3 4 5 6 |
Button { text: "Close" highlighted: { true } onClicked: { timer.start(); console.log("Button was clicked") } } |
Implementacja wewnątrz slotów podlega składni JavaScript. Kompendium informacji o tym, gdzie można stosować JavaScript w QML, znajdziecie w tym miejscu dokumentacji.
Bardzo ciekawym zastosowaniem JS w QML jest konwersja obiektu do formatu JSON lub stworzenie obiektu za pomocą definicji zawartej w formacie JSON. Załóżmy, że chcemy pewną strukturę danych przesłać dalej za pomocą formatu JSON – w tym celu możemy stworzyć obiekt z polami, które będą stanowić pary klucz–wartość. W programie operujemy wygodnie na danych za pomocą obiektu, natomiast gdy chcemy te dane przesłać, konwertujemy obiekt do JSON za pomocą JSON.stringify():
1 2 |
var obj = { name: "John", age: 30, city: "New York" }; console.log(JSON.stringify(obj)) |
Atrybut ID w QML
Zauważcie, że jedynie obiekt typu Timer ma ustawiony atrybut id – możemy to rozumieć jako nazwę (symbol), dzięki której będziemy się odnosić do konkretnego obiektu z dowolnego miejsca w obrębie zakresu danego komponentu (jeden plik .qml).
1 2 3 4 5 6 |
Timer { id: timer interval: 2000 // podawany w ms onTriggered: Qt.quit() } |
Dzięki temu w obrębie zakresu Window możemy odnosić się do obiektu Timer za pomocą jego id. Co więcej, obiektom Window i Button też możemy nadać własne id (jeżeli musimy je wykorzystać).
W QML możemy odnosić się do rodzica za pomocą właściwości parent lub za pomocą atrybutu id. Atrybut ten nie jest właściwością! Więcej informacji o id możecie przeczytać w dokumentacji.
Property Binding w QML
Porównaliśmy properties do zmiennych w klasie C++. Jednak w przypadku QML właściwości mają dużo potężniejsze możliwości – można powiedzieć, że wykorzystując Property Binding, mechanizm sygnałów i slotów działa automatycznie w tle. Dokumentacja mówi, że:
Property bindings are a core feature of QML that lets developers specify relationships between different object properties. When a property's dependencies change in value, the property is automatically updated according to the specified relationship.
Najlepiej pokazać to na przykładzie:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
import QtQuick 2.12 import QtQuick.Window 2.12 import QtQuick.Controls 2.12 Window { visible: true width: 640 height: 480 title: qsTr("Hello World") Row { anchors.centerIn: parent spacing: 10 TextField { id: textField width: 200 } Button { id: button width: 200 text: textField.text } } } |
Działanie takiego programu będzie wyglądało następująco:
W przykładzie tym uzależniliśmy właściwość text przycisku od właściwości text pola tekstowego w taki sposób, że każda zmiana w polu tekstowym powoduje zmianę tekstu na przycisku. Relacje mogą być bardziej skomplikowane i zawierać warunki logiczne, np.:
1 2 3 |
height: parent.height / 2 height: Math.min(parent.width, parent.height) height: parent.height > 100 ? parent.height : parent.height/2 |
Warto poeksperymentować z tą funkcjonalnością, bo daje ona ogromne możliwości. To tylko część opcji, które zapewnia nam QML (na poznanie kolejnych przyjdzie czas). Na przykład w C++ możemy zdefiniować właściwości, które będą reprezentować stan wewnętrzny obiektu. Właściwości te będziemy mogli zastosować w Property Binding, co pozwoli na automatyczne odświeżanie informacji.
Porównanie QML z Qt Widgets
Dla testu stworzyłem za pomocą Qt Widgets program realizujący te same wymagania co wcześniejszy przykład napisany w QML (kod obu projektów znajduje się w załączniku).
Dla Qt Widgets konieczne były 3 pliki (.h, .cpp, .ui), liczba linii (nie uwzględniając .ui) wyniosła ~80, w QML był to 1 plik o 30 liniach kodu. Jak można zauważyć, za pomocą QML osiągamy ten sam efekt w ponad dwa razy mniejszej objętości kodu (oczywiście liczba linii kodu nie jest żadną ważną miarą, ale w tym wypadku idea jest chyba dość jasna). Ponadto kod napisany w QML jest zdecydowanie bardziej prostszy do analizy niż ten napisany w C++.
Czy w takim razie QML nie ma słabych stron? Owszem, ma. Najważniejszą jest chyba brak możliwości wyłapywania części błędów na etapie kompilacji (jak w przypadku C++), która w QML przebiega bezpośrednio przed wykonaniem danego fragmentu kodu (JIT). O błędach dowiemy się dopiero w trakcie działania programu. Jednak możliwości, które daje QML, wygrywają z jego słabymi stronami.
Tryb Design i bonusowy przykład z oknem Dialog
Zauważcie, że dotychczas budowaliśmy interfejs „programowo” – każdy jego element był tworzony za pomocą linijek kodu. Interfejs możemy jednak budować też w trybie Design, podobnie jak robiliśmy to przy użyciu Qt Widgets.
Tryb Design (dla plików .qml lub .ui.qml) aktywujemy przez naciśniecie przycisku "Design" na pasku po lewej stronie (na zdjęciu poniżej jest on właśnie aktywny i podświetlony). Przed wejściem do tego trybu należy wybrać interesujący nas plik - klikając na niego dwukrotnie w drzewie projektu (otworzy to plik w trybie edycji tekstowej, co następnie pozwoli otworzyć plik w trybie Design).
Użycie trybu Design pozwala na znaczne przyspieszenie naszej pracy. Gotowe elementy i komponenty możemy przeciągać na płótno naszej aplikacji. Po wybraniu danego elementu mamy też dostęp do jego właściwości, które możemy dosłownie dowolnie „wyklikać” (w menu po prawej stronie).
Stwórzmy teraz nowy projekt – tym razem wybierzmy typ Qt Quick Application - Scroll. Następnie wybierzmy styl aplikacji Material Light. Projekty Scroll, Stack oraz Swipe stanowią szkielety pod dany typ aplikacji. Tworzenie nowego projektu przedstawia poniższe nagranie:
Kod oraz omówienie przedstawionego programu znajdują się poniżej!
Na wcześniejszym zrzucie widać zawartość ekranu w trybie Design, która pojawi się po uruchomieniu naszego przykładu. Znajdziemy tam przycisk oraz kręcący się BusyIndicator. Po kliknięciu przycisku zobaczymy dostosowane przez nas okno typu Dialog z informacją o tym, że wystąpił błąd (którego numer będziemy za każdym razem losować). Zamknięcie okna z informacją nastąpi tylko wtedy, gdy użytkownik potwierdzi Zrozumiałem i kliknie OK – wygląda to tak:
Zamiana koloru akcentów interfejsu
Gdy uruchomicie poniższy kod u siebie to akcent Waszych kontrolek na interfejsie będzie koloru różowego (Material Pink) – u mnie akcent ma barwę indygo (Material Indigo). Taką zmianę można dokonać w qtquickcontrols2.conf. Wystarczy sprawdzić zawartość nowo powstałego pliku w zakładce Resources: qml.qrc/qtquickcontrols2.conf.
Ja zmieniłem nieco jego zawartość na taką, która znajduje się poniżej:
1 2 3 4 5 6 |
[Controls] Style=Material [Material] Theme=Light Accent=Indigo |
Istotna zmiana, jaką wprowadziłem, to odkomentowanie linii, w której zdefiniowany jest akcent naszej aplikacji – podałem, że ma to być kolor indygo.
Za pomocą tego pliku możemy dostosować globalnie styl naszej aplikacji. Powiem na ten temat nieco więcej w kolejnej części kursu.
Kod tej aplikacji jest nieco dłuższy, ale równie prosty co poprzedniej:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
import QtQuick 2.12 import QtQuick.Controls 2.12 ApplicationWindow { id: applicationWindow visible: true width: 640 height: 480 Column { anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: parent.verticalCenter spacing: 10 // Kolejność definiowania obiektów, definiuje ich kolejność wyświetlania! BusyIndicator { anchors.horizontalCenter: parent.horizontalCenter } Button { text: "Wygeneruj losowy błąd" highlighted: true anchors.horizontalCenter: parent.horizontalCenter // tutaj otwieram okno dialogowe: onClicked: dialog.open() } } Dialog { id: dialog // Zdefiniowanie własnego property: property int error: 0 anchors.centerIn: parent width: parent.width * 0.7 modal: true title: "Uwaga!" closePolicy: Dialog.NoAutoClose onAboutToShow: { checkBox.checked = false error = Math.round(Math.random() * 1000) } Column { id: column spacing: 20 anchors.fill: parent Label { id: label text: "Nieoczekiwanie wystąpił błąd numer <b>" + dialog.error + "</b>. Potwierdź przeczytanie informacji." wrapMode: Text.WordWrap textFormat: Text.RichText width: parent.width } CheckBox { id: checkBox text: "Zrozumiałem" anchors.right: parent.right } Row { anchors.right: parent.right spacing: 20 DialogButtonBox { id: ok enabled: checkBox.checked standardButtons: Dialog.Ok onClicked: { // tutaj emituje sygnał, który mogę wykorzystać "wyżej" dialog.accepted() // tutaj zamykam okno dialogowe dialog.close() } } // Możemy zdefiniować więcej przycisków z różnymi funkcjami: // DialogButtonBox { // // ... // standardButtons: Dialog.Close // // ... // } } } } } |
Zastosowałem tutaj typy Column i Row oraz ich połączenie – są to podstawowe elementy, dzięki którym możemy ułożyć komponenty interfejsu w odpowiednim porządku. W QML bardzo ważna jest kolejność definiowania obiektów, np. wewnątrz takich typów jak Column czy Row. Kolejność definiowania określa ich kolejność wyświetlania w takim kontenerze. Gdy zdefiniujemy np. dwa przyciski bezpośrednio w Window, to ten zdefiniowany jako drugi przysłoni nam pierwszy – jest to związane z właściwością z.
W programie tym wykorzystałem również typy Dialog, CheckBox oraz DialogButtonBox. Warto zwrócić uwagę, że tym razem głównym elementem jest ApplicationWindow, a nie Window.
W typie Dialog dodałem zawartość w postaci CheckBoxa, Labela i DialogButtonBoxa, które zostały umieszczone w konfiguracji elementów Row i Column, tak aby stworzyły ładnie wyglądający interfejs. Wykorzystałem sygnał onAboutToShow() typu Dialog, aby odznaczyć CheckBoxa i wylosować nowy numer błędu. Ustawiłem też właściwości: modal: true oraz closePolicy: Dialog.NoAutoClose.
Istotny fragment znajduje się też w DialogButtonBoxie, gdzie zastosowałem Property Binding dla właściwości enabled, a na kliknięcie wywołuję metodę close() oraz emituję sygnał accepted(), który można byłoby wykorzystać gdzieś w aplikacji, np. gdybym miał przyciski Tak/Nie, to mógłbym emitować odpowiednie dla nich sygnały i wykorzystać je w logice. Ważniejsze fragmenty opisałem w komentarzach. Zachęcam do wprowadzania własnych zmian i testowania efektów.
Zadanie dodatkowe
Osoby, które chciałyby poznać lepiej ten temat, powinny zainteresować się poniższymi materiałami:
- Wprowadzenie do QML wykonane przez KDAB (specjalistów w dziedzinie Qt) – zapewniam, że Jesper Pedersen zdecydowanie lepiej wyjaśni niektóre omówione tutaj elementy QML.
- QmlBook – najlepsza (i do tego darmowa) książka o QML. Warto zapoznać się ze spisem treści i przeczytać rozdziały 1 oraz 4.
- Artykuł dotyczący ważniejszych funkcjonalności QML – od nagłówka QML (Qt Markup Language).
- Warto również przeczytać podlinkowane w tym artykule fragmenty dokumentacji QML.
Podsumowanie
W tej części poznaliśmy nowy sposób na budowanie nowoczesnych interfejsów w Qt, a także możliwości QML i Qt Quick. Porównaliśmy Qt Widgets z QML, realizując te same wymagania.
W następnej części zaznajomimy się dokładniej ze strukturą projektu oraz poznamy jedną z najlepszych cech QML, czyli połączymy interfejs użytkownika napisany w QML z logiką biznesową napisaną w C++. Poznamy też sposób na przesyłanie danych oraz obsługę mechanizmu sygnałów i slotów między QML a C++. Dowiemy się również, jak łatwo można wybrać styl graficzny aplikacji, np. Material, Imagine czy Universal, w celu nadania natywnego wyglądu aplikacji na docelowym urządzeniu.
Autor: Mateusz Patyk
Nawigacja kursu
O Autorze
Autorem tej serii wpisów jest Mateusz Patyk, który zawodowo zajmuje się programowaniem systemów wbudowanych oraz rozwijaniem aplikacji na desktopy i urządzenia mobilne. Jego głównymi obszarami zainteresowań są systemy sterowania, egzoszkielety i urządzenia do wspomagania chodu człowieka. Prywatnie miłośnik dobrego kina i gier strategicznych.
Powiązane wpisy
C, interfejs, programowanie, QML, qt
Trwa ładowanie komentarzy...