Search code examples
qtqmlqabstractlistmodel

How to update changes in SingleTon type custom QAbstractListModel on QML ListView?


I'm very new to QML, so having a struggle about how to propagate the changes in a custom QAbstractListModel to QML List View.

I have the following HackNewsModel.

The header file

#ifndef HACKNEWSMODEL_H
#define HACKNEWSMODEL_H

#include "Singleton.hpp"
#include <QAbstractListModel>
#include <QJsonObject>
#include <QDateTime>

struct HackNews
{
    QString m_id;
    bool m_deleted;
    QString m_type;
    QString m_by;
    QDateTime m_time;
    QString m_text;
    bool m_dead;
    QString m_parentId;
    QString m_pollId;
    QStringList m_kidsIdList;
    QString m_url;
    QString m_score;
    QString m_title;
    QStringList m_partsIdList;
    QString m_descendantCount;
};

class HackNewsModel : public QAbstractListModel, public Singleton<HackNewsModel>
{
    Q_OBJECT

public:
    void addHackNews(QJsonObject &hackNews);
    enum Roles {
        IdRole = Qt::UserRole + 1
    };

    QHash<int, QByteArray> roleNames() const override;

    int rowCount(const QModelIndex& parent = QModelIndex()) const override;

    QVariant data(const QModelIndex& index, int role/* = Qt::DisplayRole*/) const override;

    friend class Singleton<HackNewsModel>;
    explicit HackNewsModel(QObject * parent = nullptr);
    ~HackNewsModel() override;

private:
    QList<HackNews> m_hackNewsList;
    QHash<int, QByteArray> m_roles;

};

#endif // HACKNEWSMODEL_H

The Cpp file.

#include "HackNewsModel.h"
#include <QJsonArray>
#include <QDebug>

HackNewsModel::HackNewsModel(QObject *parent) : QAbstractListModel(parent)
{
    m_roles[0] = "id";

    QString id = "Demo id";
    bool deleted = false;
    QString type;
    QString by;
    QDateTime time;
    QString text;
    bool dead = false;
    QString parentId;
    QString pollId;
    QStringList kidsIdList;
    QString url;
    QString score;
    QString title;
    QStringList partsIdList;
    QString descendantCount;
    m_hackNewsList.append(HackNews{id+"1", deleted, type, by, time, text, dead, parentId, pollId, kidsIdList, url, score, title, partsIdList, descendantCount});
    m_hackNewsList.append(HackNews{id+"2", deleted, type, by, time, text, dead, parentId, pollId, kidsIdList, url, score, title, partsIdList, descendantCount});
    m_hackNewsList.append(HackNews{id+"3", deleted, type, by, time, text, dead, parentId, pollId, kidsIdList, url, score, title, partsIdList, descendantCount});
    m_hackNewsList.append(HackNews{id+"4", deleted, type, by, time, text, dead, parentId, pollId, kidsIdList, url, score, title, partsIdList, descendantCount});
    m_hackNewsList.append(HackNews{id+"5", deleted, type, by, time, text, dead, parentId, pollId, kidsIdList, url, score, title, partsIdList, descendantCount});
}

HackNewsModel::~HackNewsModel()
{

}

void HackNewsModel::addHackNews(QJsonObject &hackNews)
{
    QString id = "Demo id";
    bool deleted = false;
    QString type;
    QString by;
    QDateTime time;
    QString text;
    bool dead = false;
    QString parentId;
    QString pollId;
    QStringList kidsIdList;
    QString url;
    QString score;
    QString title;
    QStringList partsIdList;
    QString descendantCount;

    if(hackNews.contains("id"))
    {
        id = hackNews["id"].toString();
    }

    if(hackNews.contains("deleted"))
    {
        deleted = hackNews["deleted"].toBool();
    }

    if(hackNews.contains("type"))
    {
        type = hackNews["type"].toString();
    }
    if(hackNews.contains("by"))
    {
        by = hackNews["by"].toString();
    }

    if(hackNews.contains("time"))
    {
        time = QDateTime::fromTime_t(static_cast<unsigned int>(hackNews["time"].toInt()));
    }

    if(hackNews.contains("text"))
    {
        text = hackNews["text"].toString();
    }

    if(hackNews.contains("dead"))
    {
        dead = hackNews["dead"].toBool();
    }

    if(hackNews.contains("parent"))
    {
        parentId = hackNews["parent"].toString();
    }

    if(hackNews.contains("poll"))
    {
        pollId = hackNews["poll"].toString();
    }

    if(hackNews.contains("kids"))
    {
        foreach (QVariant value, hackNews["kids"].toArray().toVariantList()) {
            kidsIdList.append(value.toString());
        }
    }

    if(hackNews.contains("url"))
    {
        url = hackNews["url"].toString();
    }

    if(hackNews.contains("title"))
    {
        title = hackNews["title"].toString();
    }

    if(hackNews.contains("parts"))
    {
        foreach (QVariant value, hackNews["parts"].toArray().toVariantList()) {
            partsIdList.append(value.toString());
        }
    }

    if(hackNews.contains("descendents"))
    {
        descendantCount = hackNews["descendents"].toString();
    }

    m_hackNewsList.append(HackNews{id, deleted, type, by, time, text, dead, parentId, pollId, kidsIdList, url, score, title, partsIdList, descendantCount});
}

QHash<int, QByteArray> HackNewsModel::roleNames() const
{
    return m_roles;
}

int HackNewsModel::rowCount(const QModelIndex &parent) const
{
    if (parent.isValid())
        return 0;
    return m_hackNewsList.size();
}


QVariant HackNewsModel::data(const QModelIndex &index, int /*role*/) const
{
    //    if (!hasIndex(index.row(), index.column(), index.parent()))
    if(!index.isValid())
        return QVariant();

    const HackNews &news = m_hackNewsList.at(index.row());

    //    if(role == IdRole){
    //        qDebug() << "Seeking id";
    return news.m_id;
    //    }

    //    return QVariant();
}

However, this data model gets updated through NetworkRequestMaker that makes some request to a network and updates the model.

Header file of NetworkRequestMaker.

#ifndef NETWORKREQUESTMAKER_H
#define NETWORKREQUESTMAKER_H

#include <QObject>
#include <QNetworkAccessManager>

class QNetworkReply;
class NetworkRequestMaker : public QObject
{
    Q_OBJECT

public:
    explicit NetworkRequestMaker(QObject *parent = nullptr);
    void startRequest(const QUrl &requestedUrl);
    void httpReadyRead();
    void httpFinished();

private:
    QUrl url;
    QNetworkAccessManager m_qnam;
    QNetworkReply *m_reply;
};

#endif // NETWORKREQUESTMAKER_H

Cpp file.

#include "NetworkRequestMaker.h"
#include <QNetworkReply>
#include <QDebug>
#include <QJsonDocument>
#include <QJsonArray>
#include <QJsonObject>
#include <QDateTime>
#include "HackNewsModel.h"

NetworkRequestMaker::NetworkRequestMaker(QObject *parent)
    : QObject(parent),
      m_reply(nullptr)
{
    startRequest(QUrl("https://hacker-news.firebaseio.com/v0/item/8863.json?print=pretty"));
    startRequest(QUrl("https://hacker-news.firebaseio.com/v0/item/2921983.json?print=pretty"));
    startRequest(QUrl("https://hacker-news.firebaseio.com/v0/item/121003.json?print=pretty"));
    startRequest(QUrl("https://hacker-news.firebaseio.com/v0/item/192327.json?print=pretty"));
    startRequest(QUrl("https://hacker-news.firebaseio.com/v0/item/126809.json?print=pretty"));
    startRequest(QUrl("https://hacker-news.firebaseio.com/v0/item/160705.json?print=pretty"));
}

void NetworkRequestMaker::startRequest(const QUrl &requestedUrl)
{
    url = requestedUrl;
    m_reply = m_qnam.get(QNetworkRequest(url));
    connect(m_reply, &QNetworkReply::finished, this, &NetworkRequestMaker::httpFinished);
    connect(m_reply, &QIODevice::readyRead, this, &NetworkRequestMaker::httpReadyRead);
}

void NetworkRequestMaker::httpReadyRead()
{
    QString strReply = QString(m_reply->readAll());
    QJsonDocument jsonResponse = QJsonDocument::fromJson(strReply.toUtf8());
    QJsonObject jsonObj = jsonResponse.object();
    HackNewsModel::getInstance().addHackNews(jsonObj);
}

void NetworkRequestMaker::httpFinished()
{
    if (m_reply->error()) {
        qDebug()<<tr("Download failed:\n%1.").arg(m_reply->errorString());
    }
}

The singleton class is as the following.

#ifndef SINGLETON_HPP
#define SINGLETON_HPP

template <typename T>
class Singleton
{
public:

    /*!*************************************************************************
    \brief      Constructs the singleton (if necessary) and returns the pointer.
    ****************************************************************************/
    static T& getInstance()
    {
        static T _singleton; //!< Unique instance of class T
        return _singleton;
    }

protected:

    /*!*************************************************************************
    \brief      Constructor.
    \note       protected to avoid misuses.
    ****************************************************************************/
    Singleton() {}

    /*!*************************************************************************
    \brief      Destructor.
    \note       protected to avoid misuses.
    ****************************************************************************/
    virtual ~Singleton() {}

    /*!*************************************************************************
    \brief      Copy constructor.
    \note       protected to avoid misuses.
    ****************************************************************************/
    Singleton(const Singleton&);

    /*!*************************************************************************
    \brief      Assignment operator.
    \note       protected to avoid misuses.
    ****************************************************************************/
    Singleton& operator=(const Singleton&);
};

#endif // SINGLETON_HPP

Main cpp file is as below.

#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include "NetworkRequestMaker.h"
#include "HackNewsModel.h"

int main(int argc, char *argv[])
{
    QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
    QGuiApplication app(argc, argv);

    NetworkRequestMaker testRequestMaker;

    qmlRegisterType<HackNewsModel>("Hacknews", 1, 0, "HackNewsModel");

    QQmlApplicationEngine engine;
    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();
}

My QML file is as below.

import QtQuick 2.12
import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12
import Hacknews 1.0

Frame {
    width: 640
    height: 480
    ListView {
        id: listView
        anchors.fill: parent
        model: HackNewsModel {}

        delegate: Text {
            text: model.id
        }
    }
}

Despite the model being a singleton, qmnl listview doesn't show the updated entires. How do I enable it to show the updated entries?

Thanks.


Solution

  • Part one: QAbstractListModel interface

    First of all, your HackNewsModel needs to signal that a row has been added. Add the following to your void addHackNews method

    void HackNewsModel::addHackNews(QJsonObject &hackNews)
    {
        ...
    
        beginInsertRows(QModelIndex(), rowCount(), rowCount());        
        m_hackNewsList.append(HackNews{id, deleted, type, by, time, text, dead, parentId, pollId, kidsIdList, url, score, title, partsIdList, descendantCount});
        endInsertRows();
    }
    

    The beginInsertRows method expects the following:

    • parent model index. In your case this is an invalid index, the list is flat.
    • start index of added. Set this to the size of the list (which is 1 higher than the zero-based index of the last item)
    • end inded of added. Set this to the start index, since you are only adding one item.

    If you will be adding more functionality to your HackNewsModel make sure you implement the other begin* and end* pairs as well.

    See docs: https://doc.qt.io/qt-5/qabstractitemmodel.html#beginInsertRows

    Part two: QML singleton

    Secondly, the way you have implemented the Singleton pattern doesn't mean anything to the QML Engine. You need to tell the engine that the class is singleton:

    qmlRegisterSingletonType<HackNewsModel>("HackNews", 1, 0, "HackNewsModel",
        [](QQmlEngine *eng, QJSEngine *js) -> QObject *
        {
            eng->setObjectOwnership(&HackNewsModel::getInstance(),
                                   QQmlEngine::ObjectOwnership::CppOwnership);
            return &HackNewsModel::getInstance();
        });
    

    Note: setting the ownership might not be obligatory

    This then also means you cannot instantiate the HackNewsModel as you do in your QML, if I'm correct, the following should work:

    ListView {
        id: listView
        model: HackNewsModel
        ...
    }