Kurs Qt – #6 – Przesyłanie danych między QML i C++

Kurs Qt – #6 – Przesyłanie danych między QML i C++

W poprzedniej części poznaliśmy technologie Qt Quick i QML. Tym razem zajmiemy się użyciem tych nowości od strony C++.

Przyjrzymy się strukturze projektu i zawartości pliku main.cpp. Później połączymy warstwę C++ z QML i zaczniemy przesyłać między nimi dane.


Struktura projektu Qt Quick

Na początek tworzymy nowy projekt: New project → Qt Quick Application - Scroll (na razie wybieramy styl, np. Material Light). Docelowy styl może być dowolny; ważne, żeby któryś wybrać, by Qt Creator stworzył nam plik konfiguracyjny qtquickcontrols2.conf, który pojawił się już w poprzedniej części kursu.

Projekt tego typu składa się z pliku projektu (z rozszerzeniem .pro) oraz trzech katalogów:

  • Headers – katalog na pliki .h (obecnie go nie ma, bo nie posiadamy plików nagłówkowych).
  • Sources – katalog na pliki .cpp (znajduje się w nim m.in. plik main.cpp).
  • Resources – katalog na pliki, które staną się częścią pliku wynikowego programu, czyli np. pliki z rozszerzeniem .qml, .ui.qml, ikony, pliki tłumaczeń czy wspomniany plik konfiguracyjny.

Katalog Resources, a właściwie Qt Resource System (System Zasobów Qt), jest przydatny, gdy Twoja aplikacja potrzebuje określonego zestawu plików (ikon, plików tłumaczeń itp.) i nie chcesz, aby te pliki zostały usunięte lub uległy zmianie. Umieszczając je w systemie zasobów, kontrolujesz je jedynie na etapie rozwoju aplikacji, a przy jej dystrybucji pliki te znajdą się w pliku wynikowym aplikacji. Więcej informacji o systemie znajdziesz w tej dokumentacji. My wykorzystamy system zasobów, aby umieścić tam ikony przycisków.

Przejdźmy teraz do początku naszego nowego projektu, czyli pliku main.cpp – jego zawartość prezentuje się następująco:

Omówimy poszczególne fragmenty tego pliku:

  • QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling) – to włączenie trybu skalowania na ekranach o dużym DPI. Na komputerach pewnie nie zobaczymy różnicy między włączoną a wyłączoną opcją, natomiast na telefonach już tak.
  • QGuiApplication app(argc, argv) – to stworzenie obiektu naszej aplikacji, która zarządza przepływem sterowania i głównymi ustawieniami naszego GUI.
  • QQmlApplicationEngine engine – to stworzenie obiektu naszego silnika QML (w zasadzie jest to wrapper na silnik QML), który pozwala na załadowanie interfejsu naszej aplikacji z pliku QML.
  • const QUrl url(QStringLiteral("qrc:/main.qml")) – to stworzenie obiektu typu QUrl, który przechowuje lokalizację pliku main.qml naszej aplikacji; ta lokalizacja bazuje na systemie zasobów Qt, przedrostek qrc:/<file> informuje Qt, że ma szukać pliku file w systemie zasobów pod prefiksem /.
  • stwór w postaci wywołania QObject::connect(...), choć wygląda strasznie, to nic innego jak połączenie sygnału objectCreated() z silnika z naszą główną aplikacją i slotem w postaci lambdy, której zadaniem jest wyłączenie aplikacji z kodem wyjścia -1, gdy stworzony obiekt wskazuje na NULL i jego URL jest równy URL-owi naszego pliku main.qml.
  • Engine.load(url) to nic innego jak załadowanie pliku naszej aplikacji QML.
  • Na końcu uruchamiamy główną pętlę zdarzeń (event loop) naszej głównej aplikacji.

Integracja C++ w QML

Załóżmy, że budujemy aplikację, która będzie wykorzystywana do sterowania łazika. Na interfejsie chcemy mieć przyciski umożliwiające poruszanie się pojazdu: do przodu i do tyłu oraz na boki. Oprócz tego chcemy mieć możliwość ustawienia czułości sterowania i dezaktywacji panelu sterowania, a także podgląd na symboliczny status połączenia.

Interfejs może wyglądać mniej więcej tak:

Przykładowy interfejs aplikacji

Przykładowy interfejs aplikacji

A tak może wyglądać jego użycie w praktyce:

W ramach ćwiczenia zbudujemy właśnie taki interfejs. W tym celu można wykorzystać stworzony już wcześniej projekt – wystarczy usunąć zbędną zawartość z pliku main.qml. Z kolei w pliku z konfiguracją  (qtquickcontrols2.conf) wprowadzamy następujące zmiany:

Ustawiamy tym samym akcent na kolor indigo i usuwamy dalszą, zbędną zawartość pliku. Do stylów wrócimy jeszcze na końcu tej części. Teraz pora zabrać się za grafiki.

Na potrzeby tego projektu pobrałem darmowe ikonki dla przycisków ze strony stylu Material. Polecam przejrzeć wszystkie, aby móc je wykorzystać w swoich projektach. Ikony umieściłem w katalogu projektu w folderze media/icons, następnie dodałem je do projektu z poziomu Qt Creatora, klikając prawym przyciskiem myszy w drzewie projektu na qml.qrc → Add Existing Files i wybierając je wszystkie. Po potwierdzeniu powinny być widoczne w drzewie projektu:

Ikona zaimportowana do projektu

Ikona zaimportowana do projektu

Złą ikonę (w przypadku pomyłki) najlepiej usunąć z projektu za pomocą drzewa projektu, wybierając konkretny plik lub ikonę i klikając na nie PPM → Remove (następnie należy potwierdzić, czy chcemy permanentnie usunąć dany plik). Wtedy Qt Creator automatycznie edytuje zawartość pliku z zasobami. Bez konkretnego powodu staramy się nie edytować zawartości plików zasobów ręcznie. W razie problemów z plikiem zasobów (dowiemy się o tym na etapie kompilacji) konieczne może być wyczyszczenie projektu – klikamy PPM na nazwę projektu → Clean i uruchomienie qmake'a (PPM na nazwę projektu → Run qmake).

Teraz, mając gotowe zasoby, możemy przejść do napisania interfejsu. Ja zbudowałem interfejs w oparciu o typy Column, Row, Button, Slider, ToolTip i podstawowy Rectangle. Część interfejsu napisałem ręcznie, następnie – mając wszystkie przyciski umieszczone w kolumnach/rzędach – wykorzystałem tryb Design, aby wyrównać lub ustawić wszystkie elementy odpowiednio na interfejsie:

Podgląd interfejsu w trybie projektowania

Podgląd interfejsu w trybie projektowania

Spróbujcie zaimplementować ten interfejs samodzielnie. Zastanówcie się, jak połączyć typy Column i Row, aby uzyskać taki układ. Zastosujcie kotwice (anchors) oraz skorzystajcie z możliwości, jakie daje graficzny tryb Design, aby dostosować interfejs. Gotowy kod interfejsu znajdziecie poniżej (wykorzystajcie go jako wzór dla swojego). Najważniejsze elementy omówimy osobno później:

Omówienie kodu – uwagi ogólne

Część wspólnych właściwości, np. wysokość i szerokość, zdefiniowałem w jednej linii, oddzielając je średnikami – ma to wyłącznie znaczenie estetyczne. Obiektom, do których będę się odnosił w innym miejscu, nadałem ID.

Width, Height, MinimumWidth oraz MinimumHeight

Wykorzystałem te właściwości, aby nadać określone rozmiary pewnym obiektom i ustawić minimalny rozmiar okna applicationWindow za pomocą minimumWidth oraz minimumHeight (chodzi o rozmiar głównej kolumny). Niepisaną zasadą QML jest nadawanie każdemu obiektowi wysokości i szerokości – jawnie bądź niejawnie (przez zastosowanie layoutów). Gdy stworzycie jakiś obiekt i nie widzicie go na interfejsie, to prawdopodobnie nie ma on nadanych rozmiarów.

Kotwice (anchors)

Kotwice zostały wykorzystane w programie w celu wypoziomowania obiektów względem rodziców lub umieszczenia niektórych centralnie, np. column.

Ikony przycisku

Ikony przypisałem za pomocą właściwości icon.source, podając odpowiednie adresy URL dla każdego przycisku. Możemy je skopiować, klikając PPM na konkretną ikonę w drzewie projektu i wybierając Copy URL “qrc:/...” – możemy użyć adresu z prefiksem, czyli: qrc:/media/icons/icon.svg, lub go pominąć: /media/icons/icon.svg.

Definiowanie własnych właściwości (properties)

Zobaczcie na samą górę pliku pod ApplicationWindow – zdefiniowałem tam właściwość isConnected. Teraz symbolicznie reprezentuje ona status naszego połączenia z łazikiem. W tym momencie jedno jest pewne – połączenie jest bardzo stabilne. Więcej o definiowaniu właściwości przeczytacie w tej dokumentacji. Podobnie zdefiniowałem właściwość isWorking w przycisku startStop.

Aktywacja panelu sterowania

Aktywację lub dezaktywację przycisków wykonałem przez proste property binding, wiążąc właściwości enabled każdego przycisku z właściwością highlighted przycisku startStop. Natomiast highlighted przycisku startStop jest z kolei zależne od właściwości isWorking oraz isConnected.

Slider

Slider definiujący czułość naszego sterowania umieściłem pod przyciskami oraz ustawiłem go w zakresie od 0 do 100 z krokiem 0,1. W celu wyświetlenia etykiety z wartością nad suwakiem wykorzystałem bardzo ciekawy typ ToolTipa – więcej na jego temat znaleźć można w tej dokumentacji.

Stopka (wizualizacja stanu połączenia)

Ostatnim elementem jest dolna belka – footer. Zbudowałem ją z typu Rectangle oraz Row, w którym znajduje się typ BusyIndicator. Informuje on o stanie połączenia z naszym łazikiem. Jeśli się kręci – mamy połączenie; jeśli nie – połączenia nie ma. W rzędzie możemy dodać też inne indykatory.

Zauważ, w jaki sposób podałem kolor typu Rectangle – wykorzystałem globalne ustawienie (z pliku konfiguracyjnego) za pomocą stałej Material.accent. Zauważ też, że przy BusyIndicator zmieniłem lokalnie wartość akcentu na biały. Do stylów jeszcze wrócimy.

Przesyłanie informacji między C++ i QML

Najtrudniejsza część, czyli zbudowanie interfejsu, już za nami. Przejdźmy teraz do tej przyjemnej części, czyli logiki biznesowej naszej aplikacji. Dobrą praktyką jest zawsze stworzenie najpierw szkieletu interfejsu aplikacji, a następnie napisanie odpowiednich klas logiki biznesowej. Takie podejście da nam pogląd na wymagania naszej aplikacji oraz na to, jakich danych i funkcjonalności będziemy potrzebować.

Zacznijmy od napisania klasy, która będzie wirtualnie sterować naszym łazikiem – wirtualnie, ponieważ jedyne, co będzie robić, to drukować w konsoli daną czynność. Żeby realnie sterowała naszym łazikiem, musielibyśmy zaimplementować komunikację – ale to już umiemy robić (szczegóły w części 4 kursu)!

Klasa sterująca łazikiem

Napiszmy więc prostą klasę z odpowiednimi metodami sterującymi – nazwijmy ją Rover. Ważne jest, aby klasa dziedziczyła po klasie QObject.

Klikamy PPM na nazwę projektu w drzewie projektów i wybieramy Add new C++ Class. Ustawienie klasy bazowej jako QObject dla naszej klasy przedstawia poniższy zrzut ekranu:

Ustawienia klasy bazowej

Ustawienia klasy bazowej

Zawartość naszej klasy

Plik nagłówkowy:

Plik źródłowy:

W klasie dodałem dwie zmienne: isConnected oraz controlLevel, czyli status połączenia z łazikiem oraz czułość sterowania. Zaimplementowałem też metody sterujące naszym łazikiem jako publiczne sloty.

Zarejestrowanie obiektu C++ w QML

Teraz, gdy mamy działającą klasę sterującą naszym łazikiem, wystawmy obiekt tej klasy do QML. W tym celu przechodzimy do pliku main.cpp i wykorzystujemy nasz obiekt silnika QQmlApplicationEngine. Pierwszą czynnością jest załączenie dwóch plików nagłówkowych:

Następnie tworzymy obiekt naszej klasy:

Ostatnią czynnością jest zarejestrowanie kontekstu (wystawienie danych) naszego obiektu w głównym kontekście silnika (w głównym obiekcie naszego interfejsu). W skrócie: poniższa linia rejestruje nasz obiekt rover klasy Rover w QML pod nazwą rover:

Pozwala to na użycie w QML naszego obiektu, np. w taki sposób:

W celu wyjaśnienia powyższego możemy posłużyć się dokumentacją:

  • rootContext():
    Returns the engine's root context. The root context is automatically created by the QQmlEngine. Data that should be available to all QML component instances instantiated by the engine should be put in the root context.
  • setContextProperty(const QString &name, QObject *value):
    Set the value of the name property on this context.

Zawartość pliku main.cpp jest teraz następująca:

Użycie zarejestrowanego obiektu C++ w QML

Teraz możemy powrócić do QML i wykorzystać nasz wystawiony obiekt. Pod każdym przyciskiem dodajcie odpowiednie fragmenty, np. pod przyciskiem goForward:

Po zarejestrowaniu obiektu QtCreator powinien nam pomóc w podpowiadaniu składni:

Podpowiadanie składni w praktyce

Podpowiadanie składni w praktyce

Następnie uruchomcie program i sprawdźcie, czy w konsoli po kliknięciu lub puszczeniu przycisku pojawiają się odpowiednie komunikaty. Takim właśnie sposobem z poziomu QML jesteśmy w stanie wywoływać metody obiektów C++.

Definiowanie właściwości w C++

Nasz interfejs jeszcze nie działa tak, jak byśmy tego chcieli – nie mamy statusu połączenia i nie przekazujemy czułości sterowania do naszego obiektu klasy Rover. W tym celu stworzymy właściwości w klasie, których będziemy mogli używać w QML (tak jak używamy właściwości w QML). W tym celu w klasie Rover dodajemy:

W makrze Q_PROPERTY podajemy zmienne i metody, które zostały zdefiniowane wcześniej w klasie. W naszym przykładzie chodzi nam o zmienną controlLevel, którą odczytujemy za pomocą metody getControlLevel(), a zmieniamy za pomocą metody setControlLevel(), natomiast o jej zmianie informujemy za pomocą sygnału controlLevelChanged(). Więcej informacji znajdziemy w przykładach z dokumentacji.

Teraz w QML możemy w łatwy sposób zmieniać poziom czułości bezpośrednio w naszym obiekcie klasy Rover. W tym celu dodajmy w typie Slider fragment kodu:

Podobną czynność wykonamy teraz ze stanem połączenia z naszym łazikiem. Dodamy sobie trochę więcej pracy i zasymulujemy utratę i wznawianie połączenia. Wykorzystamy w tym celu klasę QTimer, w której będziemy losować interwał zegara w pewnych zakresach – zakresy te będą reprezentować czas aktywnego połączenia i czas jego wznawiania. Generatorem liczb losowych jest tu QRandomGenerator.

Nowe fragmenty w klasie Rover. Plik .h:

Plik .cpp:

Szczególnie istotne po stronie C++ jest wywołanie emisji sygnału na każdą zmianę parametru, do którego przypisana jest właściwość – jeśli w QML nic się nie zmienia, oznacza to, że zapewne nie emitujemy nigdzie sygnału o tej zmianie!

Teraz w pliku main.qml możemy wykorzystać właściwość isConnected:

Takim sposobem zbudowaliśmy w pełni działający przykład. Połączenie będzie trwało 5–20 s, natomiast jego brak – między 0,5 a 4 s. Na belce interfejsu kręcący się BusyIndicator będzie teraz pokazywać status połączenia, a przyciski będą się podświetlać w zależności od tego, czy połączenie i panel sterowania są aktywne. Działanie interfejsu przedstawia poniższe nagranie:

Jakie typy można przesyłać między C++ i QML?

Między QML i C++ możemy wymieniać dane – w przypadku prostych lub standardowych typów wymiana jest prosta i nie wymaga dodatkowych kroków. Natomiast w przypadku typów niestandardowych konieczne jest wykonanie pomocniczych operacji. Więcej informacji o wymianie danych między C++ i QML znajdziemy w tej dokumentacji.

Obsługa sygnałów z domeny C++ w QML

W QML możemy również łatwo obsługiwać sygnały w C++ – służy do tego element Connections:

Nadanie natywnego wyglądu aplikacji

Wróćmy teraz do stylu aplikacji. Qt jest międzyplatformowym frameworkiem, dlatego wspiera też różne style, które są domyślne na tych platformach, np. Material na Androidzie czy Universal na platformach Windows. Mamy pełną dowolność w wyborze domyślnego dla aplikacji trybu, m.in. za pomocą wspomnianego wcześniej pliku konfiguracyjnego. Zmieniając style w pliku konfiguracyjnym, możemy porównać wygląd tego samego interfejsu w różnych stylach:

Więcej informacji o stylach znaleźć można w tej dokumentacji, a o właściwościach każdego z nich na dedykowanych im podstronach. Wartość właściwości każdego ze stylów, np. accent, możemy nadać w pliku konfiguracyjnym (w jednym miejscu), a następnie używać ich jako stałych w naszej aplikacji. Takim sposobem kontrolujemy wygląd aplikacji w jednym miejscu.

Zadanie dodatkowe

Osoby, które chciałyby zagłębić się w ten temat, powinny:

  • zapoznać się z Coding Convention dla QML,
  • zaznajomić się z treścią dokumentacji użytych w przykładach typów QML i zagadnień,
  • poeksperymentować z przesyłaniem informacji między C++ i QML,
  • przeczytać artykuł dotyczący lambda expresions w C++ i nowej składni metody connect w Qt5,
  • uruchomić przykład Qt Quick Controls - Gallery i zapoznać się z dostępnymi stylami i kontrolkami.

Czy wpis był pomocny? Oceń go:

Średnia ocena 4.9 / 5. Głosów łącznie: 18

Nikt jeszcze nie głosował, bądź pierwszy!

Artykuł nie był pomocny? Jak możemy go poprawić? Wpisz swoje sugestie poniżej. Jeśli masz pytanie to zadaj je w komentarzu - ten formularz jest anonimowy, nie będziemy mogli Ci odpowiedzieć!

Podsumowanie

W tej części omówiliśmy strukturę projektu i zawartość pliku main.cpp. Następnie połączyliśmy dwie domeny naszej aplikacji: warstwę logiki biznesowej (C++) z warstwą interfejsu użytkownika (QML). Poznaliśmy też różne sposoby na przesyłanie informacji między tymi warstwami. Na końcu omówiliśmy zagadnienie stylu aplikacji oraz poznaliśmy standardowe style, które wspiera Qt.

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.

Załączniki

C, kurs, programowanie, qt

Trwa ładowanie komentarzy...