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:

    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
        ...
    }