C++QtQMLJavaScript

Das Bilderrahmen Programm (1)

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.

asyncio.h
#ifndef ASYNCIO_H
#define ASYNCIO_H

#include "typdef.h"

#include <QFile>
#include <QDataStream>
#include <QtConcurrent/QtConcurrent>

class AsyncIO : public QObject
{
    Q_OBJECT
public:
    AsyncIO(QObject *parent = nullptr);
    
    void save(QString, const QByteArray &);    
    void load(const QString &);    

signals:
    void readFinished(QByteArray);      
    void writeFinished(QString);
    void error(QString);
};

#endif // ASYNCIO_H

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.

asyncio.cpp
#include "asyncio.h"

AsyncIO::AsyncIO(QObject *parent) : QObject(parent) {}

void AsyncIO::save(QString filepath, const QByteArray &data)
{
    QtConcurrent::run([=]() {
        QFile file(filepath);
        if (file.open(QIODevice::WriteOnly)) {
            file.write(data);
            file.close();
            emit writeFinished(filepath);
        }
        else {
            emit error(QString("Couldn't open '%1'").arg(filepath));
        }
    });
}

void AsyncIO::load(const QString &filepath) {
    QtConcurrent::run([=]() {
        QFile file(filepath);
        if (file.exists() && file.open(QIODevice::ReadOnly)) {
            QTextStream stream(&file);
            QString line;
            QByteArray data;

            while (stream.readLineInto(&line)) {
                data.append(line.toStdString().c_str());
            }

            file.close();
            emit readFinished(data);
        }
        else if(!file.exists()) {
            emit error(QString("File '%1' doesn't exists").arg(filepath));
        }
        else {
            emit error(QString("Couldn't open '%1'").arg(filepath));
        }
    });
}

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.

picture.h
#ifndef PICTURE_H
#define PICTURE_H

#include <QObject>
#include <qqml.h>
#include <QJsonDocument>
#include <QJsonObject>

class Picture : public QObject
{
  Q_OBJECT
  Q_ENUMS(FillModeIndices)
  Q_ENUMS(BlendModeIndices)
  Q_PROPERTY(qint16 blendModeIndex READ blendModeIndex WRITE setBlendModeIndex NOTIFY blendModeChanged)
  Q_PROPERTY(qint16 fillModeIndex READ fillModeIndex WRITE setFillModeIndex NOTIFY fillModeChanged)
  Q_PROPERTY(qint16 fillMode READ fillMode NOTIFY fillModeChanged)
  Q_PROPERTY(QString blendMode READ blendMode NOTIFY blendModeChanged)
  Q_PROPERTY(QString file READ file WRITE setFile NOTIFY fileChanged)  
  QML_ELEMENT

public:
  explicit Picture(QObject *parent = nullptr);

  QJsonObject toJsonObject();
  QString file() const;
  QString blendMode() const;
  qint16 blendModeIndex() const;
  qint16 fillModeIndex() const;
  qint16 fillMode() const;  

  void setFile(const QString &);
  void setFillModeIndex(const int &);
  void setBlendModeIndex(const int & = 0);

  enum FillModes {
    Stretch, PreserveAspectFit,
    PreserveAspectCrop, Tile, Pad = 6 };
  enum FillModeIndices {
      STRETCH, FIT, CROP, PAD, TILE };
  enum BlendModeIndices {
      NORMAL, LIGHTEN, SOFTLIGHT, SCREEN,
      COLOR, COLORBURN, COLORDODGE, EXCLUSION, MULTIPLY };

  static Picture* fromJsonObject(QJsonObject);
  static Picture* create(QString);
  static QHash<int, QString> BLENDING;
  static QHash<int, int> FILLING;

signals:
  void fileChanged();
  void blendModeChanged();
  void fillModeChanged();

private:
    QString m_file;
    qint16 m_blendMode;
    qint16 m_fillMode;

};

#endif // PICTURE_H

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.

picture.cpp
QHash<int, QString> Picture::BLENDING {
  {Picture::BlendModeIndices::NORMAL, "normal"},
  {Picture::BlendModeIndices::LIGHTEN, "lighten"},
  {Picture::BlendModeIndices::SOFTLIGHT, "softLight"},
  {Picture::BlendModeIndices::SCREEN, "screen"},
  {Picture::BlendModeIndices::COLOR, "color"},
  {Picture::BlendModeIndices::COLORBURN, "colorBurn"},
  {Picture::BlendModeIndices::COLORDODGE, "colorDodge"},
  {Picture::BlendModeIndices::EXCLUSION, "exclusion"},
  {Picture::BlendModeIndices::MULTIPLY, "multiply"}
};

QHash<int, int> Picture::FILLING {
  {Picture::FillModeIndices::STRETCH, Picture::FillModes::Stretch},
  {Picture::FillModeIndices::FIT, Picture::FillModes::PreserveAspectFit},
  {Picture::FillModeIndices::CROP, Picture::FillModes::PreserveAspectCrop},
  {Picture::FillModeIndices::PAD, Picture::FillModes::Pad}
};

Die Getter / Setter Methoden aus den Q_PROPERT(Y)ies

Diese Methoden werden aufgerufen, wenn die jeweilige Eigenschaft in QML gelesen werden soll.

picture.cpp
QString Picture::file() const {
  return m_file;
}

QString Picture::blendMode() const {
  return Picture::BLENDING[m_blendMode];
}

qint16 Picture::blendModeIndex() const {
  return m_blendMode;
}

qint16 Picture::fillModeIndex() const {
  return m_fillMode;
}

qint16 Picture::fillMode() const {
  return Picture::FILLING[m_fillMode];
}

Und diese Methoden werden beim Setzen der Eigenschaft aus dem QML Kontext raus aufgerufen.

picture.cpp
void Picture::setFile(const QString &file) {
  if (file == m_file)
    return;

  m_file = file;
  emit fileChanged();
}

void Picture::setBlendModeIndex(const int &mode)
{
    if (mode == m_blendMode)
        return;

    m_blendMode = static_cast<qint16>(mode);
    emit blendModeChanged();
}

void Picture::setFillModeIndex(const int &mode)
{
    if (mode == m_fillMode)
        return;

    m_fillMode = static_cast<qint16>(mode);
    emit fillModeChanged();
}

Dabei wird wieder mit Qt's Signal/Slot System die im Header deklarierten signal Methoden getriggered.

JSON Import/Export

picture.cpp
QJsonObject Picture::toJsonObject() {
  QJsonObject json;
  json["file"] = m_file;
  json["blend_mode"] = m_blendMode;
  json["fill_mode"] = m_fillMode;

  return json;
}

Picture* Picture::fromJsonObject(QJsonObject json) {
  Picture *pic = new Picture();

  if (!json["file"].isUndefined())
    pic->setFile(json["file"].toString());

  if (!json["blend_mode"].isUndefined())
    pic->setBlendModeIndex(json["blend_mode"].toInt());

  if (!json["fill_mode"].isUndefined())
    pic->setFillModeIndex(json["fill_mode"].toInt());

  return pic;
}

Die vollständig implementierte Klasse

picture.cpp
#include "picture.h"

QHash<int, QString> Picture::BLENDING {
  {Picture::BlendModeIndices::NORMAL, "normal"},
  {Picture::BlendModeIndices::LIGHTEN, "lighten"},
  {Picture::BlendModeIndices::SOFTLIGHT, "softLight"},
  {Picture::BlendModeIndices::SCREEN, "screen"},
  {Picture::BlendModeIndices::COLOR, "color"},
  {Picture::BlendModeIndices::COLORBURN, "colorBurn"},
  {Picture::BlendModeIndices::COLORDODGE, "colorDodge"},
  {Picture::BlendModeIndices::EXCLUSION, "exclusion"},
  {Picture::BlendModeIndices::MULTIPLY, "multiply"}
};

QHash<int, int> Picture::FILLING {
  {Picture::FillModeIndices::STRETCH, Picture::FillModes::Stretch},
  {Picture::FillModeIndices::FIT, Picture::FillModes::PreserveAspectFit},
  {Picture::FillModeIndices::CROP, Picture::FillModes::PreserveAspectCrop},
  {Picture::FillModeIndices::PAD, Picture::FillModes::Pad}
};

Picture::Picture(QObject *parent) : QObject(parent) {
  setBlendModeIndex(0);
  setFillModeIndex(Picture::FillModes::PreserveAspectCrop);
}

QString Picture::file() const {
  return m_file;
}

QString Picture::blendMode() const {
  return Picture::BLENDING[m_blendMode];
}

qint16 Picture::blendModeIndex() const {
  return m_blendMode;
}

qint16 Picture::fillModeIndex() const {
  return m_fillMode;
}

qint16 Picture::fillMode() const {
  return Picture::FILLING[m_fillMode];
}

void Picture::setFile(const QString &file) {
  if (file == m_file)
    return;

  m_file = file;
  emit fileChanged();
}

void Picture::setBlendModeIndex(const int &mode) {
    if (mode == m_blendMode)
        return;

    m_blendMode = static_cast<qint16>(mode);
    emit blendModeChanged();
}

void Picture::setFillModeIndex(const int &mode) {
    if (mode == m_fillMode)
        return;

    m_fillMode = static_cast<qint16>(mode);
    emit fillModeChanged();
}

QJsonObject Picture::toJsonObject() {
  QJsonObject json;
  json["file"] = m_file;
  json["blend_mode"] = m_blendMode;
  json["fill_mode"] = m_fillMode;

  return json;
}

Picture* Picture::fromJsonObject(QJsonObject json) {
  Picture *pic = new Picture();

  if (!json["file"].isUndefined())
    pic->setFile(json["file"].toString());

  if (!json["blend_mode"].isUndefined())
    pic->setBlendModeIndex(json["blend_mode"].toInt());

  if (!json["fill_mode"].isUndefined())
    pic->setFillModeIndex(json["fill_mode"].toInt());

  return pic;
}

Picture* Picture::create(QString file) {
  Picture *pic = new Picture();
  pic->setFile(file);

  return pic;
}

Weitere Blog Posts aus dieser Serie

  1. Embedded Linux Image mittels Yocto erstellen
  2. Das Bilderrahmen Programm (1)
  3. Das Bilderrahmen Programm (2)