C++QtQML

Das Bilderrahmen Programm (2)

In diesem Artikel werden wir den C++ Teil der Anwendung zu einem vorläufigen Ende bringen. Neben dem Einsprungspunkt des Programms, werden wir auch eine Schnittstelle zum QML Interface programmieren.

Das C++ Grundgerüst war das letzte Mal noch nicht komplett fertig. Wir benötigen noch eine Klasse, die unsere Bildliste repräsentiert und als Schnittstelle zwischen Betriebssystem und QML Interface dienen soll. Dazu werden wir die Picture Klasse mittels Makro als Type für QML registrieren. Außerdem schreiben wir noch den Einsprungspunkt für unsere Anwendung. Damit bleibt für den dritten Teil nur noch der QML - Part.

Die Schnittstelle von C++ zu QML

Die Picture Klasse aus dem letzten Blog Post repräsentiert ein Bild im Speicher der Anwendung. Das Bild liegt jedoch noch nicht im Arbeitsspeicher. Es geht eigentlich darum den Speicherort und die Darstellungseigenschaften des Bildes zu manipulieren, speichern und an die QML Engine weiterzureichen. Das Laden des eigentlichen Bildes und die grafische Darstellung wird komplett von dem QML Code gesteuert. Damit QML auch weiß was eigentlich das Picture Objekt ist und was es kann, müssen wir es als Typ registrieren. Dabei hilft uns der Meta Object Compiler (MOC) und einige Macros.


Die Pictures Klasse

Sie repräsentiert eine Liste von Picture Elementen und stellt außer Methoden zum Hinzufügen, Ändern und Entfernen auch zum Speichern und Laden zur Verfügung. Zudem ist sie die Schnittstelle zu QML und im späteren Verlauf auch zu dem TCP Socket Server.

pictures.h
#ifndef PICTURES_H
#define PICTURES_H

#include <QObject>
#include <QJsonDocument>
#include <QJsonArray>
#include <QQmlPropertyMap>
#include <QtConcurrent/QtConcurrent>
#include <QTcpSocket>
#include <qqml.h>

#include "picture.h"
#include "settings.h"

Q_DECLARE_METATYPE(Picture*)
Q_DECLARE_METATYPE(QQmlListProperty<Picture*>)

class Pictures : public QObject
{
    Q_OBJECT    
    Q_PROPERTY(QQmlListProperty<Picture> list READ list NOTIFY picturesChanged)
    Q_PROPERTY(int count READ count NOTIFY picturesChanged)
    Q_PROPERTY(int index READ index WRITE setIndex NOTIFY indexChanged)
    Q_CLASSINFO("DefaultProperty", "list")
    QML_ELEMENT


public:
    Pictures(QObject *parent = nullptr);

    void toJsonDoc(QJsonDocument &);
    QJsonDocument toJsonDoc() const;
    QQmlListProperty<Picture> list();
    int count() const;
    int index() const;
    QString rmItem(const int);
    QString getLastError() const;    

    Q_INVOKABLE Picture* pic(int) const;
    Q_INVOKABLE void _save();

    void clear();
    void append(Picture *);
    void replace(int, Picture *);
    void removeLast();
    void add(const QString &, const QByteArray &);
    void add(QString, QString, const QByteArray &);
    void setIndex(const int &);
    void remove(const int);
    void remove(const int, QTcpSocket *);
    void move(const int, const int);    
    void load();
    void save(QString *error = nullptr, int i = -1, QTcpSocket *sender = nullptr);

signals:
    void picturesChanged();
    void indexChanged();
    void removed(const int, QTcpSocket *);
    void imageSaved(const QString &);
    void error(const QString &);

public slots:
    void onImageSaved(QString);
    void onLoaded(QByteArray);
    void onError(const QString &);

private:
    static QJsonDocument toJsonDoc(QVector<Picture*>*);
    static void append(QQmlListProperty<Picture>*, Picture*);
    static int count(QQmlListProperty<Picture>*);
    static Picture* pic(QQmlListProperty<Picture>*, int);
    static void clear(QQmlListProperty<Picture>*);
    static void replace(QQmlListProperty<Picture>*, int, Picture*);
    static void removeLast(QQmlListProperty<Picture>*);

    int m_index = 0;
    QVector<Picture *> m_pics;
    QString m_lastError;
    AsyncIO io;
};

#endif // PICTURES_H

Das Q_DECLARE_METATYPE Makro lässt den MOC die Picture Klasse für den Zugriff in QML registrieren und mit dem Q_INVOKABLE Makro können dann die jeweiligen Methoden aus dem QML Kontext aufgerufen werden.


Die Implementation

pictures.cpp
#include "pictures.h"

Pictures::Pictures(QObject *parent) : QObject(parent) {
    connect(&io, &AsyncIO::readFinished, this, &Pictures::onLoaded);
    connect(&io, &AsyncIO::error, this, &Pictures::onError);
    connect(&io, &AsyncIO::writeFinished, this, &Pictures::onImageSaved);
    load();
}

Im Constructor verbinden wir drei Signale von der AsyncIO Klasse mit Slots der Pictures Klasse. Beim Erstellen kümmert sich mal wieder der Meta Object Compiler um den nötigen Boilerplate Code. Zudem wird die load() Methode aufgerufen.

pictures.cpp
void Pictures::toJsonDoc(QJsonDocument &json) {
  QFuture<void> future = QtConcurrent::run([&]() {
      QJsonArray arr;
      QVector<Picture*>::const_iterator i;
      for (i = m_pics.begin(); i != m_pics.cend(); ++i) {          
          arr.append((*i)->toJsonObject());
      }

      json = QJsonDocument(arr);
  });
}

QJsonDocument Pictures::toJsonDoc() const {
  QJsonArray arr;
  QVector<Picture*>::const_iterator i;

  for (i = m_pics.begin(); i != m_pics.cend(); ++i) {
      arr.append((*i)->toJsonObject());
  }

  return QJsonDocument(arr);
}

Die toJsonDoc Methode und ihre überladene Schwester erstellt uns ein JSON Objekt zum Speichern und versenden der Bilddaten. Der Code Abschnitt im Lambda Ausdruck, der als Parameter von QtConcurrent::run übergeben wurde, wird in einem neuem Thread ausgeführt.

pictures.cpp
QQmlListProperty<Picture> Pictures::list() {
  return {this, this,
               &Pictures::append,
               &Pictures::count,
               &Pictures::pic,
               &Pictures::clear,
               &Pictures::replace,
               &Pictures::removeLast};
}

Die list() Methode gibt ein QQmlListProperty<Picture> Objekt zurück, das auf die aktuelle Instanz der Pictures Klasse und einigen ihrer Methoden verweist.

pictures.cpp
Picture* Pictures::pic(int i) const {
  return m_pics.at(i);
}

void Pictures::append(Picture* p) {
  m_pics.append(p);
  io.save(PICTURES_FILE, toJsonDoc().toJson());
  emit picturesChanged();
}

int Pictures::count() const {
  return m_pics.count();
}

int Pictures::index() const {
  return m_index;
}

void Pictures::replace(int i, Picture* p) {
  m_pics.replace(i, p);
  emit picturesChanged();
}

void Pictures::clear() {
  m_pics.clear();
  emit picturesChanged();
}

void Pictures::removeLast() {
  return m_pics.removeLast();
}

void Pictures::append(QQmlListProperty<Picture>* list, Picture* p) {
  reinterpret_cast<Pictures* >(list->data)->append(p);
}

void Pictures::clear(QQmlListProperty<Picture>* list) {
  reinterpret_cast<Pictures* >(list->data)->clear();
}

void Pictures::replace(QQmlListProperty<Picture> *list, int i, Picture *p) {
  reinterpret_cast<Pictures* >(list->data)->replace(i, p);
}

void Pictures::removeLast(QQmlListProperty<Picture> *list) {
  reinterpret_cast<Pictures* >(list->data)->removeLast();
}

Picture* Pictures::pic(QQmlListProperty<Picture>* list, int i) {
  return reinterpret_cast<Pictures* >(list->data)->pic(i);
}

int Pictures::count(QQmlListProperty<Picture>* list) {
  return reinterpret_cast<Pictures* >(list->data)->count();
}

Die statischen Methoden können aus dem QML Kontext aufgerufen werden und verweisen auf die entsprechenden Methoden der instanszierten Klasse.

pictures.cpp
void Pictures::move(const int from, const int to) {  
  Picture *buffer = m_pics.at(from);
  m_pics.replace(from, m_pics.at(to));
  m_pics.replace(to, buffer);
  _save();
  emit picturesChanged();
}

void Pictures::add(const QString &file, const QByteArray &data) {
   QString filepath = QString("%1/%2").arg(IMAGES_DIR).arg(file);
   QString filename = QString("file:/%1").arg(filepath);

   if (m_pics[0]->file() == "")
    m_pics.remove(0);

   QDir path(SAVE_DIR);

   if(path.exists() || path.mkpath(SAVE_DIR)) {
       m_pics.append(Picture::create(filename));

       io.save(filepath, data);
       io.save(PICTURES_FILE, toJsonDoc().toJson(QJsonDocument::JsonFormat::Compact));

       emit picturesChanged();
   }
}

void Pictures::add(QString file, QString id, const QByteArray &data) {
   QString filepath = QString("%1/%2").arg(IMAGES_DIR).arg(file);
   QString filename = QString("file:/%1").arg(filepath);

   if (m_pics[0]->file() == "")
    m_pics.remove(0);

   QDir path(SAVE_DIR);

   if(path.exists() || path.mkpath(SAVE_DIR)) {
       m_pics.append(Picture::create(filename));

       io.save(filepath, id, data);
       io.save(PICTURES_FILE, toJsonDoc().toJson(QJsonDocument::JsonFormat::Compact));

       emit picturesChanged();
   }
}

void Pictures::_save() {
  io.save(PICTURES_FILE, toJsonDoc().toJson(QJsonDocument::JsonFormat::Compact));
  qDebug() << "_save";
}

QString Pictures::rmItem(const int i) {
   QString file(m_pics[i]->file().mid(6));
   m_pics.remove(i);   
   return file;
}

void Pictures::remove(const int i, QTcpSocket *client) {
   if (-1 < i && i < m_pics.length()) {
        const QVector<Picture *> list(m_pics);
        const QString file(rmItem(i));
        qDebug() << file;
        if (QFile::exists(file)) {
            if (QFile::remove(file)) {               
                _save();
                emit removed(i, client);
                emit picturesChanged();
            }
            else {
                m_pics = list;
                m_lastError = QString(tr("couldn't delete '%1'")).arg(file);
                _save();
            }
        }
        else {
            m_pics.remove(i);
            io.save(PICTURES_FILE, toJsonDoc().toJson(QJsonDocument::JsonFormat::Compact));
            m_lastError = tr("file not found");
            emit error(m_lastError);
            emit picturesChanged();
        }
    }
    else {
        m_lastError = tr("index out of range");
        emit error(m_lastError);        
    }  
}

void Pictures::setIndex(const int &i) {
  int len = count();

  if (i >= 0 && i < len) {
      m_index = i;
  }
  else if (i >= len) {
      m_index = 0;
  }
  else if (i < 0) {
      m_index = len - 1;
  }

  emit indexChanged();
}

QString Pictures::getLastError() const {
  return m_lastError;
}

void Pictures::load() {
    io.load(PICTURES_FILE);
}

Diese Methoden werden später für den Server benötigt. Ihre Funktionalität entspricht den jeweiligen Namen.

pictures.cpp
void Pictures::onImageSaved(QString cid) {
  emit imageSaved(cid);
}

void Pictures::onLoaded(QByteArray data) {
  if (data.length()) {
    QFuture<void> future = QtConcurrent::run([=]() {
        QJsonDocument json = QJsonDocument::fromJson(data.data());
        QJsonArray arr = json.array();
        QJsonArray::const_iterator i;

        for (i = arr.begin(); i != arr.cend(); ++i) {
          m_pics.append(Picture::fromJsonObject((*i).toObject()));          
        }        
        emit picturesChanged();
    });

    future.waitForFinished();
  }
  else {
    m_pics.append(Picture::create(""));
  }
}

void Pictures::onError(const QString &msg) {
    qDebug() << msg;
    m_lastError = msg;
    emit error(msg);
}

Abschließend sind hier die drei Methoden, die im Constructor mit den Signalen von der AsyncIO Klasse verbunden wurden. Erwähnenswert ist allerdings einzig die onLoaded Methode. Sie erstellt aus den JSON Objekten die Datenstruktur der Bilderliste.


Einsprungpunkt der Anwendung

Für alle die jetzt ein wenig verwirrt drein Blicken; Die Main Funktion ist der Einsprungspunkt und somit die Stelle an dem die Ausführung des Programms beginnt.

In C++ wäre das int main() { /* ...* / return 0; }.

main.cpp
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QObject>
#include "typdef.h"
#include "pictures.h"
#include <QDebug>

int main(int argc, char *argv[])
{
    qputenv("QT_IM_MODULE", QByteArray("qtvirtualkeyboard"));
    
    Pictures pictures;    

    qmlRegisterInterface<Picture>("Picture", 0);        

    QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
    QCoreApplication::setOrganizationDomain("michm.de");
    QCoreApplication::setApplicationName("Digital Picture Frame");

    QGuiApplication app(argc, argv);
    QQmlApplicationEngine engine;    

    engine.rootContext()->setContextProperty("pictures", &pictures);

    const QUrl url(QStringLiteral("qrc:/main.qml"));

    QObject::connect(
      &engine, &QQmlApplicationEngine::objectCreated,
      &app, [url](QObject *obj, const QUrl &objUrl)
      {      
        if (!obj && url == objUrl)
            QCoreApplication::exit(-1);
      }, Qt::QueuedConnection);  

    engine.load(url);    

    return app.exec();
}

Nach dem Erstellen eines Pictures Objekts registrieren wir die Picture Klasse für den QML Kontext, damit aus diesem dann auf die Eigenschaften zugegriffen werden kann.

Danach wird je eine Instanz von QGuiApplication und QQmlApplicationEngine erzeugt. QGuiApplication ist vor allem für den Eventloop des Programms zuständig. QQmlApplicationEngine kümmert sich um das Ausführen des QML Codes.

Die setContextProperty Methode des rootContext Objekts erstellt letztendlich für QML eine Eigenschaft, die sich dort ähnlich wie ein JavaScript Objekt verhält. Der erste Parameter ist ein String für den Namen und der zweite ist ein Pointer zu unserer Pictures Instanz.


Weitere Blog Posts aus dieser Serie

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