1
votes

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?

1
Going through C++ seems like a giant wasted effort. Since JS natively supports JSON and you can use a simple JS array as a model for a list view... - dtech
I don't see any implementation for public slot fetch() called in qml Timer tickerClient.fetch() - Mohammad Kanan
@MohammadKanan I didn't add it to my question, because it is not important to understand the problem. As you can see, the data returned by a call to fetch() is handeled in onReceive(), which extracts the json data from the http response and pushes it to the list of QVariantMaps. - Phidelux
@dtech There are a couple of reasons I believe (don't know) it's not a good idea to use JS here. I am not familar with JS and I don't see an effective way of parallising a massive amount of http request. Furthermore I am not sure how to handle errors like missing or changed model entities in Javascript. So, why would I use qml instead of Qt Widgets? I like the modern UI elements like QDrawer and QStackView, which are not available as Widgets. My general impression is that Widget UI elements are somehow dated in look and feel. However, I am open for alternatives. - Phidelux
Well, you are already using QML for the GUI, you might as well make good use of it. Everything that C++ code does is possible and much easier to do in QML, you can make the request, parse the returned JSON and use the result as a model. All that stuff is supported natively in JS. In this particular case using C++ wins you nothing, no tangible performance gains when your bottleneck is http, only increase in complexity and slower development speed as you have to keep recompiling for every minor change. - dtech

1 Answers

1
votes

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