Framework Qt to nie tylko szereg mniej lub bardziej przydatnych klas do tworzenia interfejsu użytkownika. Qt wraz z wbudowanym generatorem Meta-Object Compiler odpowiada za przebudowę definicji naszych klas w planowany sposób. W tym wpisie przyjrzymy się rozwiązaniom przyjętym przez twórców w tym między innymi klasie QObject.
Uwaga
Zawodowo zajmuję się tematem Qt Development. Od czasu gdy napisałem te wpisy minęło kilka lat a ja zdążyłem założyć jednąz najbardziej znanych firm zajmujących się kodzeniem w Qt. Ten tutorial trochę się zaktualizował, więc zachęcam Cię odwiedzić stronę Scythe Studio, bo tam dzielimy się nowym tutorialem na YouTube, w którym temat jest podejmowany.
Zanim zaczniesz
W ramach tego kursu Qt w pierwszej kolejności skupiamy się na tworzeniu interfejsu graficznego korzystając właśnie z tej technologii. Następnie to czytelnicy będą mogli zdecydować, w którą stronę pójdzie kurs. Dołożę wszelkich starań, aby nowe wpisy ukazywały się co czwartek. Jeżeli ten wpis jest pierwszym z tego kursu na jaki trafiasz to sugeruję, abyś zajrzał również do poprzednich. Miłej lektury!
- Aplikacje okienkowe. Czym jest Qt?
- Instalacja frameworka Qt
Czym jest Meta-Object Compiler?
Meta-Object Compiler (w skrócie MOC) to system, który na podstawie deklaracji klas generuje kod źródłowy (.cpp) w folderze build projektu. Generowanie następuje praktycznie przy każdorazowej kompilacji zatem nie zmieniamy własnoręcznie zawartości plików. Właściwie to nawet nas ona nie interesuje – wygenerowany kod wykorzystywany jest jedynie przez mechanizmy frameworka, takie jak system tzw. properties oraz system sygnałów i slotów, który opiszemy sobie w kolejnej części tego kursu Qt. Dodatkowo system zapewnia nam takie możliwości jak:
- QObject::metaObject() – metoda zwracająca meta dane obiektu, a w tym Run-Time Type Information (RTTI);
- QMetaObject::className() – metoda zwracająca nazwę klasy obiektu, co może być przydatne, gdy korzystamy chociażby z zamkniętych bibliotek dołączonych statycznie.
- QObject::inherits(const char* className) – metoda zwracająca prawdę jeżeli obiekt na rzecz, którego wywołujemy metodę dziedziczy to klasie wskazanej jako paramter;
- QObject::tr() – metoda zapewniająca wygodny system do tłumaczenia aplikacji;
- QMetaObject::newInstance() – metoda umożliwiająca stworzenie nową instancję danej klasy. Przydatne, gdy sami piszemy kod w sposób generyczny.
Jednym słowem Meta-Object Compiler zapewnia wygodny i efektywny sposób na wyjście poza możliwości C++.
Jak wykorzystać potencjał Meta-Object Compiler?
Jeżeli piszesz swój program w oparciu o Qt i chcesz wykorzystać jego możliwości, nie obędziesz się bez obrania ścieżki proponowanej przez twórców. Na szczęście, jeżeli używasz systemu qmake, korzystanie z MOC nie wymaga od Ciebie dodatkowego ingerowania w kod. W momencie, gdy korzystasz z CMake to warto dodać w pliku CMakeList.txt następującą własność:
1 |
AUTOMOC ON |
Pozwoli to na automatycznie generowanie plików przez Meta-Object Compiler.
Przechodzimy do części praktycznej. MOC generuje kod na podstawie definicji klasy i utworzony plik przez siebie plik nazywa w następującym formacie: moc_ClassName.cpp
. Niemniej, aby generacja mogła mieć miejsce, nasza klasa musi w części prywatnej zawierać makro takie jak Q_OBJECT czy Q_GADGET. Na ten moment skupimy się na tym pierwszym.
Q_OBJECT jest makrem, które musi występować we wspomnianym miejscu jeżeli chcemy korzystać z systemu sygnałów i slotów oraz pozostałych możliwości. Rzućmy okiem na przykładową definicję klasy:
1 2 3 4 5 6 7 8 9 10 11 |
#include <QObject> class Engine : public QObject { Q_OBJECT public: Engine(); private: int _acceleration; } |
Dodanie makra to jedyny obowiązek spoczywający na programiście. Jeżeli klasa dodatkowo dziedziczy po podstawowej klasie frameworka, czyli QObject to można sobie darować dodawanie tego marka. Niemniej oficjalna dokumentacja sugeruje, aby dodawać je zawsze, dlatego tak właśnie będziemy postępować.
Real time type information z Qt
Teraz możemy już skorzystać z systemu Meta-Object. Do clue sprawy przejdziemy w następnym wpisie poświęconym sygnałom i slotom będącymi głównym powodem wprowadzenia narzędzia MOC. Na ten moment rzucimy okiem jak możemy wykorzystać informację na temat typu podczas działania programu.
1 2 3 |
Engine engine; qDebug() << "Nazwa klasy: " << engine.metaObject()->className(); // Engine qDebug() << "Nazwa klasy bazowej: " << engine.metaObject()->superClass()->className(); // QObject |
Przykładowo w taki sposób moglibyśmy posiąść informację na temat klasy obiektu czy jego klasy nadrzędnej. W przypadku, gdy jakaś klasa nie miałaby klasy bazowej to metodą superClass() zwróciłaby nullptr.
Klasa QObject
Matka wszystkich innych klas w Qt. Jeżeli chcesz efektywnie korzystać z tego frameworka i wykorzystywać jego funkcje to z pewnością będziesz wielokrotnie dziedziczył bezpośrednio po klasie QObject albo po jej klasach pochodnych. Z tego powodu spróbujemy zaznajomić się z podstawowymi aspektami tej klasy.
Mechanizm rodzicielstwa
Domyślny konstruktor klasy QObject wygląda następująco:
QObject(QObject *parent = nullptr)
Jak sam widzisz tworząc obiekt klasy QObject mamy możliwość przekazania jako parametr wskaźnika do innego obiektu tejże klasy lub klasy pochodnej do niej. Obiekt przekazany jako rodzic doda do swojej listy dzieci (QObjectList children) wskaźnik na dopiero co utworzony obiekt.
Po co to wszystko? Takie podejście umożliwia nam dostęp do rodzica obiektu za pomocą metody parent() oraz do jego dzieci za pomocą metody children(). Dzięki temu nie musimy tworzyć dodatkowych wskaźników do rodzica/dzieci w definicji klasy a następnie dbać o zwalnianie pamięci. System rodzicielstwa w Qt robi to za nas.
Jak mechanizm rodzicielstwa działa pod spodem?
Dzięki systemowi rodzicielstwa nie musimy dbać o zwalnianie pamięci w sposób manualny. W destruktorze klasy QObject możemy znaleźć wywołanie poniższej funkcji:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
void QObjectPrivate::deleteChildren() { Q_ASSERT_X(!isDeletingChildren, "QObjectPrivate::deleteChildren()", "isDeletingChildren already set, did this function recurse?"); isDeletingChildren = true; // delete children objects // don't use qDeleteAll as the destructor of the child might // delete siblings for (int i = 0; i < children.count(); ++i) { currentChildBeingDeleted = children.at(i); children[i] = 0; delete currentChildBeingDeleted; } children.clear(); currentChildBeingDeleted = nullptr; isDeletingChildren = false; } |
Przeanalizuj go na spokojnie. Jak łatwo się domyślić jej założeniem jest zwolnienie pamięci okupywanej przez dzieci danego obiektu. Osobiście uwielbiam to rozwiązanie. Pamiętaj jednak, że ten mechanizm zadziała jedynie jeżeli przy tworzeniu obiektu podamy wskaźnik do rodzica.
Tak mogłaby wyglądać prosta klasa wykorzystująca ten mechanizm:
1 2 3 4 5 6 7 8 9 10 11 |
class Engine : public QObject { Q_OBJECT public: Engine(QObject *parent = nullptr) : QObject(parent) { // ... } private: int _acceleration; } |
Natomiast tak wyglądałaby inicjalizacja obiektu tej klasy z rodzicem:
1 2 |
QObject parent; Engine *engine = new Engine(&parent); |
Q_DISABLE_COPY
Qt zakłada, że wszystkie instancje klasy QObject oraz klas pochodnych powinny być rozpatrywane jako pojedyncze twory przez co nie ma możliwości ich kopiowania. Takie zachowanie obiektów jest celowe i wręcz wskazane z uwagi na:
- System sygnałów i slotów, wiążący ze sobą konkretne obiekty. Skopiowanie takiego połączenia mogłoby skutkować nieprzewidywalnym rezultatem.
- Mechanizm rodzicielstwa, który zakłada, że obiekt może mieć jednego rodzica i wiele dzieci. Pytanie tylko co w sytuacji, gdy obiekt ma kilka dzieci. Kopia takiego obiektu łamałaby tą zasadę, ponieważ dzieci musiałyby mieć więcej niż jednego rodzica lub musiałyby zostać również skopiowane co byłoby nieefektywne.
- System własności (properties) – temat do omówienia przy okazji integracji kodu w C++ z Qml, ale na ten moment jedyne co musimy wiedzieć to to, że lista properties (pewnego rodzaju atrybutów klasy) może być rozszerzona w trakcie działania programu. Pytanie zatem co z dodanymi w ten sposób własnościami. Czy powinny również być skopiowane? Wbrew pozorom nie jest to takie oczywiste.
W związku z powyższym kod taki jak ten:
1 2 3 4 5 6 |
class Engine : public QObject { Q_OBJECT }; Engine engine; Engine copy = engine; |
Skutkowałby poniższym błędem kompilacji:
„error: use of deleted function 'Engine::Engine(const Engine&)’”
Dzieje się tak z uwagi na fakt, że używane jest makro Q_DISABLE_COPY(Class) służące właśnie do obsługi tego zachowania. Na dobrą sprawę nie ma konieczności dodawania tego makra do definicji utworzonych klas niemniej wówczas błędy kompilatora będą bardziej czytelne.
1 2 3 4 |
class Engine : public QObject { Q_OBJECT Q_DISABLE_COPY(Engine) }; |
W praktyce makro Q_DISABLE_COPY przenosi konstruktor kopiujący i operator przypisania do sekcji prywatnej i (w zależności od wersji C++) oznacza te metody jako delete. Oczywiście dzieje się to automatycznie i w definicjach naszych klasach nie ma potrzeby zamieszczania żadnych zmian.
1 2 3 4 5 6 |
class MyClass : public QObject { private: MyClass(const MyClass &) = delete; MyClass &operator=(const MyClass &) = delete; }; |
Dziedzicz po QObject tylko wtedy, gdy jest to przydatne
Pamiętaj, że jeżeli twoja klasa służy np. wyłącznie jako struktura danych to nie ma potrzeby, aby dziedziczyła po klasie QObject lub zawierała makro Q_OBJECT. Powiedziałbym, że w takich przypadkach dziedziczenie po klasie QObject jest bezpodstawne tym bardziej, że obiektów klasy QObject nie można kopiować co przy klasach zawierających jedynie dane bez większych funkcjonalności staje się problemem.
Podsumowanie
To by było na tyle jeżeli chodzi o tą część naszego kursu Qt wprowadzającego w architekturę i rozwiązania stojące za tą technologią. Moim celem numer jeden jest przedstawienie tematu w sposób szczegółowy tak, abyś w pełni zrozumiał mechanizmy stojące za tym frameworkiem. W następnym wpisie poruszymy inny aspekt dotyczący Meta-Object Compiler i klasy QObject, jakim jest mechanizm sygnałów i slotów. Daj znać w komentarzu czego się dowiedziałeś i co jeszcze chciałbyś zobaczyć w ramach tego kursu, a jeżeli chcesz dowiedzieć się więcej na temat klasy QObject to odsyłam do dokumentacji. [https://doc.qt.io/qt-5/qobject.html]