Phidelux
Phidelux

Reputation: 2271

Displaying Json data with qml ListView

I am currently working on a ticker client, that polls data from a web api, hands it to a ListModel, which is then used to display the data in a qml ListView.

class TickerClient : public QObject
{
    Q_OBJECT

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

    QList<QVariantMap> items() const;

protected:
    void registerErrorHandlers(QNetworkReply *reply);

signals:
    void statusChanged(QNetworkAccessManager::NetworkAccessibility acc);
    void dataChanged();

    void preItemRefresh();
    void postItemRefresh();

public slots:
    void fetch(int start = 0, int limit = 100);

protected slots:
    void onReceive(QNetworkReply *reply);

protected:
    QSharedPointer<QNetworkAccessManager> mNetMan;
    QList<QVariantMap> mItems;
};

class TickerModel : public QAbstractListModel
{
    Q_OBJECT
    Q_PROPERTY(TickerClient *client READ client WRITE setClient)

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

    enum {
        IdRole = Qt::UserRole + 1,
        NameRole,
        SymbolRole,
        ...
    };

    // Basic functionality:
    int rowCount(const QModelIndex &parent = QModelIndex()) const override;

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

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

    TickerClient *client() const;
    void setClient(TickerClient *client);

private:
    TickerClient *mClient;
};

The ticker client does not only fetch, but also handle the fetched data and expose it to the surrounding ListModel as a list of QVariantMaps.

void TickerClient::onReceive(QNetworkReply *reply)
{
    if (!reply)
        return;

    if(reply->error()) {
        qCritical() << "Error: "
                    << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toString();
        return;
    }

    //  Read all data as json document.
    QJsonDocument jsonDoc = QJsonDocument::fromJson(reply->readAll());

    if(!jsonDoc.isArray()) {
        qCritical() << "Error: Expected array";
        return;
    }

    emit preItemRefresh();

    mItems.clear();

    QJsonArray currencies = jsonDoc.array();
    for(int i = 0; i < currencies.count(); i++) {
        QJsonObject currency = currencies[i].toObject();
        mItems.append(currency.toVariantMap());
    }

    emit postItemRefresh();

    reply->deleteLater();
}

Both, the TickerClient and the TickerModel are exposed to qml:

qmlRegisterType<TickerModel>("Ticker", 1, 0, "TickerModel");
qmlRegisterUncreatableType<TickerClient>("Ticker", 1, 0, "TickerClient",
        QStringLiteral("MarketCapProvider should not be created in QML"));

TickerClient tickerClient;

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

The exposed client is then handed to the model and refreshed every 5 seconds:

ListView {
    id: page

    Timer {
        interval: 5000
        running: true
        repeat: true
        triggeredOnStart: true
        onTriggered: {
            var pos = scrollBar.position
            tickerClient.fetch()
            scrollBar.position = pos
        }
    }

    ScrollBar.vertical: ScrollBar {
        id: scrollBar
    }

    model: TickerModel {
        client: tickerClient
    }

    delegate: RowLayout {
        width: parent.width
        spacing: 10

        Label {
            text: "#" + model.rank
            padding: 5
        }

        Label {
            text: model.name
            padding: 5
        }
    }
}

However, the fetching of the data doesn't seem to work well as the wrong data is returned. So if I ask for model.name, I might end of with model.rank. The following code is used to fetch the entry for a given pair of index and role.

QVariant TickerModel::data(const QModelIndex &index, int role) const
{
    if (!index.isValid() || !mClient)
        return QVariant();

    auto it = roleNames().find(role);
    if(it != roleNames().end())
        return mClient->items().at(index.row())[it.value()];
    else
        qDebug() << "Didn't match a role";

    return QVariant();
}

QHash<int, QByteArray> TickerModel::roleNames() const
{
    static const QHash<int, QByteArray> names{
        { IdRole, "id" },
        { NameRole, "name" },
        ...
    };

    return names;
}

TickerClient *TickerModel::client() const
{
    return mClient;
}

void TickerModel::setClient(TickerClient *client)
{
    beginResetModel();

    if(mClient) {
        mClient->disconnect(this);
    }

    mClient = client;
    if(mClient) {
        connect(mClient, &TickerClient::preItemRefresh, this, [=]() {
            beginResetModel();
        });

        connect(mClient, &TickerClient::postItemRefresh, this, [=]() {
            endResetModel();
        });
    }

    endResetModel();
}

What, am I doing wrong and how can I extend this solution for more complex Json objects?

Upvotes: 1

Views: 3527

Answers (1)

talamaki
talamaki

Reputation: 5482

You are clearing mItems and then appending new items to it in onReceive method. You are not showing if model is listening to postItemRefresh signal. Anyway, you should call beginResetModel and endResetModel.

E.g. add following slot to your TickerModel and connect it to postItemRefresh signal to get your model synched witht the new data:

void TickerModel::reset()
{
    beginResetModel();
    endResetModel();
}

Edit:

You should add prints or go through with the debugger with what parameters your model data() is called when things get screwed. Also, make sure beginResetModel and endResetModel are called when items are updated.

Is your rowCount returning mClient->items().count() ?

To handle more complex json objects, instead of:

QJsonArray currencies = jsonDoc.array();
for(int i = 0; i < currencies.count(); i++) {
    QJsonObject currency = currencies[i].toObject();
    mItems.append(currency.toVariantMap());
}

You don't handle QVariantMap but parse more complex json objects to your own class which provides getters for data:

QJsonArray currencies = jsonDoc.array();
for(int i = 0; i < currencies.count(); i++) {
    QJsonObject currency = currencies[i].toObject();
    ComplexCurrency c;
    c.read(currency);
    mItems.append(c);
}

Your class:

class ComplexCurrency
{
public:
    int id() const;
    QString name() const;
    bool enabled() const;
    QList<QVariantMap> items();

    void read(const QJsonObject &json);

private:
    int m_id;
    QString m_name;
    bool m_enabled;
    QList<QVariantMap> m_items;
    ...
};

void ComplexCurrency::read(const QJsonObject &json)
{
    m_id = json["id"].toInt();
    m_name = json["name"].toString();
    m_enabled = json["enabled"].toBool();
    // parse items array here
}

And then in model data():

QVariant TickerModel::data(const QModelIndex &index, int role) const
{
    if ((index.row() < 0 || index.row() >= mClient->items().count()) || !mClient)
        return QVariant();

    const ComplexCurrency &c = mClient->items().at(index.row());
    if (role == NameRole)
        return c.name();
    else if (role == SomethingRole)
        return c.something();

    return QVariant();
}

Upvotes: 1

Related Questions