Am Ende dieses 3 teiligen Artikels soll das Programm Bilder laden, anzeigen und nach einer bestimmten Zeit zum nächsten Bild wechseln. Darauf bauen wir dann fortlaufend auf.
In dem vorherigen Blogbeitrag habe ich erläutert wie man mittels Yocto ein Linux Image für einen Raspberry Pi kompiliert und Qt Creator als Entwicklerumgebung einrichtet um für das auf dem Pi laufenden Linux Software zu erstellen.
Das Framework Qt 5 bietet allerhand praktische Features mit denen das Entwickeln recht schnell von der Hand geht. Allerdings muss ich diejenigen, die eine kommerzielle Nutzung von Qt in Betracht ziehen, hier eine Warnung aussprechen. Zur Zeit sieht es so aus als würde die Qt Group auf ein reines Abo Model setzen wodurch einige rechtliche Geschichten im Argen liegen. Vor allem bei so "Kleinigkeiten" wie die Monetarisierung von Software auf Basis von Qt mit einem ausgelaufenen Abo.
Features der Applikation
Da auf dem Raspi ein Linux nur mit dem nötigsten läuft und der Bilderrahmen auch von Nutzern mit keinen nennenswerten IT Erfahrungen bedient werden soll, muss zu dem offensichtlichen Dingen wie zum Beispiel Bilder anzeigen, speichern, nach einer bestimmten Zeit nächstes Bild anzeigen, usw. auch noch eine Möglichkeit implementiert werden den Raspberry an ein WLAN anzumelden. Vorausgesetzt wird dazu allerdings ein Touch Display. Darüber hinaus soll er von dort auch neu gestartet und einige Basis Einstellungen geändert werden können.
Auf dem Optionen - Screen soll auch ein QR Code angezeigt werden. Dadurch lässt sich dann mittels App auf dem Smartphone leicht eine Verbindung mit ihm herstellen. Demnach kommt zu dem Standard - IO (Einstellungen und Bilder laden/speichern) auch noch ein kleiner WebSocket Server hinzu.
Die CPU Kerne des Pi's möchten beschäftigt werden. Von daher wird auf Threading gesetzt sobald es Sinn ergibt.
Das UI ist vom Rest der Anwendung abgekoppelt und wird in QML realisiert. QML ist eine Markup Sprache, die sehr stark an JSON und CSS erinnert. Inline JavaScript ermöglicht ein komplexes Verhalten der Steuerelemente zu realisieren ohne dabei auf C++ zurückgreifen zu müssen. So lassen sich mit ihr relativ schmerzfrei GUI's entwickeln.
Es können C++ Klassen in QML referenziert werden. Sogar Methoden der Klasse lassen sich in QML ausführen. Somit hat man auch eine Schnittstelle zum Betriebssystem.
QML wird normalerweise just-in-time (JIT) kompiliert. Also zur Laufzeit wird daraus Bytecode erzeugt. Allerdings besteht auch die Möglichkeit den QML Code ahead-of-time (AOT) zu erstellen. Persönlich gefällt mir besonders, das man QML mit JavaScript mischen kann.
Optional soll neben aktueller Zeit und Datum auch Wetterdaten angezeigt werden. Diese werden von OpenWeatherMap.org abgerufen. Derzeit denke ich auch noch darüber nach die Wetterdaten direkt vom DWD zu beziehen. Allerdings liegen die Daten des DWD in nur unbereinigter Form als Binärdaten zum Download bereit. Vorerst scheint mir der Aufwand zu groß.
Asynchrones speichern und laden von Daten
Wir brauchen eine Klasse die sich um's Laden und Speicher kümmert. Vorzugsweise in einen separaten Thread damit bei einer langsamen SD Card nicht doch irgendwann der UI Thread blockiert.
In der Header Datei der Klasse AsyncIO sind neben dem Konstruktor 5 weitere Methoden definiert. Soweit nichts außergewöhnliches.
Erwähnenswert ist vielleicht noch das Q_OBJECT Makro. Ich muss ein wenig ausholen um zu erklären wozu das gut ist.
Der Meta Object Compiler (MOC) von Qt liest die C++ Header Dateien und wenn er Klassen mit dem Q_OBJECT Makro findet generiert er zusätzlichen Code, der unter anderem Qt's Signal und Slot System handled. Signals und Slots sind vom Prinzip her Event Emitter und Events sehr ähnlich.
Von den 5 definierten Methoden aus der Header Datei müssen wir hier lediglich 2 implementieren. Zu den anderen kommen wir später noch.
Der QtConcurrent Namespace bietet eine Abstraktionsschicht für das Threading. Um Low Level Threading primitive wie zum Beispiel Mutexes, Semaphore und Lese - / Schreibsperren muss man sich dank QtConcurrent keine Gedanken machen.
Die Methode QtConcurrent::run() erwartet als Argument eine Funktion, welche dann in einen separaten Thread ausgeführt wird.
Die QFuture API bietet eine Schnittstelle mit der man nicht nur etwaige Rückgabewerte erhalten kann, sondern auch den kompletten Thread Zyklus steuern kann.
Ich habe hier allerdings einfach das Signal/Slot System vom QObject genutzt. Vor den Aufrufen der Methoden writeFinished(QString), readFinished(QByteArray) und error(QString) steht noch ein emit. Das ist das Zeichen für den MOC das Event an alle verbundenen Objekte zu verteilen.
Bildeigenschaften und deren Bereitstellung in einem QML Objekt
Wir definieren zwei Klassen, die Eigenschaften und Methoden zum Handling der Bilder bereitstellen sollen.
Die Klasse Picture soll ein Bild Element darstellen. Neben dem Dateipfad, hat sie zwei weitere Eigenschaften. Mit m_blendMode soll später einmal ein Mischmodus und mit m_fillMode ein Füllmodus geregelt werden.
Wir benötigen einige weitere Makros damit der MOC zaubern kann. Zum einem wäre da Q_ENUMS. Es ermöglicht einen lesenden Zugriff von in C++ deklarierten Enumerations in QML. Q_PROPERTY ermöglicht je nach Aufruf einen Lese- / Schreibzugriff auf Variablen unterschiedlichsten Typs. Sollte ein Typ nicht bekannt sein muss dieser vorab als Meta Type für den Meta Object Compiler deklariert werden. Als letztes hätten wir noch Q_ELEMENT. Dieses Makro registriert den QML Type.
Darauf folgen die Deklaration vom Konstruktor, einiger Methoden, drei Enumerations und zweier Hashmaps auf die ich gleich noch im Detail zurückkomme.
Die privaten Member m_file, m_blendMode und m_fillMode halten die weiter oben schon genannten Eigenschaften des Bildes.
Die Hashmaps
Die beiden Hashmaps werden wir brauchen um später in den Optionen den Füll- und Mischmodus in einer Combobox darstellen zu können und dabei eine Verknüpfung zu den beiden jeweiligen Eigenschaften unserer Klasse zuhaben.
Die Getter / Setter Methoden aus den Q_PROPERT(Y)ies
Diese Methoden werden aufgerufen, wenn die jeweilige Eigenschaft in QML gelesen werden soll.
Und diese Methoden werden beim Setzen der Eigenschaft aus dem QML Kontext raus aufgerufen.
Dabei wird wieder mit Qt's Signal/Slot System die im Header deklarierten signal Methoden getriggered.