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