Home > database >  Qml/Qt c : How to automatically update items in Qml ListView when the underlying Qt c ListModel c
Qml/Qt c : How to automatically update items in Qml ListView when the underlying Qt c ListModel c

Time:01-09

Based on an example video from Qt (the ToDo example), I have created a ListView based Qt/Qml application.

Data for the ListView in Qml comes from a c class based on QAbstractListModel. The c model class is populated with data from a database.

This all works fine.

I have added a pushbutton in the Qml file which invokes a method in the c code that fetches data from a remote source (i.e. makes an https request). The https response data is of course asynchronous with the method that makes the http request.

Debug lines in the c https response handler confirm that the response is received okay. I save the response to the database.

If I close the application and re-open it, the new data is shown in the Qml list because, once again, on opening the application the c model is populated from the db.

But what I really need is that, after saving the data to the db in the https response handler, I also push the new data to the Qml ListView so that I don't have to restart the application to refresh the updated list data.

Unfortunately I don't know how to push the new data from c to Qml. I have tried a number of ways (signals from c , slots in Qml, reading the updated list from c , etc) but nothing has worked so far.

I know it has to do with the fact that the pushbutton starts an http request in c which is not handled synchronously but in a slot function which is the http response handler.

But unfortunately I don't know how to resolve this issue.

I would appreciate some help with this.

Note: The following example is in fact off of an excellent YouTube Video on Qt model/view by Mitch Curtis Using C Models in QML - To-Do List!

But my code is very similar except that I want to add a button which changes the descriptions based on an https response:

The c   files providing the listdata: 
todolist.h and todolist.cpp
=============================================================


#ifndef TODOLIST_H
#define TODOLIST_H

#include <QObject>
#include <QVector>

struct ToDoItem
{
    bool done;
    QString description;
};

class ToDoList : public QObject
{
    Q_OBJECT
public:
    explicit ToDoList(QObject *parent = nullptr);

    QVector<ToDoItem> items() const;

    bool setItemAt(int index, const ToDoItem &item);

signals:
    void preItemAppended();
    void postItemAppended();

    void preItemRemoved(int index);
    void postItemRemoved();

public slots:
    void appendItem();
    void removeCompletedItems();

private:
    QVector<ToDoItem> m_Items;
};
=======================================================================
#include "todolist.h"

ToDoList::ToDoList(QObject *parent) : QObject(parent)
{
    m_Items.append({ true, QStringLiteral("Wash the car") });
    m_Items.append({ false, QStringLiteral("Fix the sink") });
    m_Items.append({ true, QStringLiteral("Wash the dishes") });
}

QVector<ToDoItem> ToDoList::items() const
{
    return m_Items;
}

bool ToDoList::setItemAt(int index, const ToDoItem &item)
{
    if (index <0 || index >= m_Items.size()) {
        return false;
    }

    const ToDoItem &oldItem = m_Items.at(index);
    bool nothingChanged = oldItem.done == item.done
                            && oldItem.description == item.description;
    if(nothingChanged) {
        return false;
    }

    m_Items[index] = item;
    return true;
}

void ToDoList::appendItem()
{
    emit preItemAppended();

    ToDoItem item;
    item.done = false;
    m_Items.append(item);

    emit postItemAppended();

}

void ToDoList::removeCompletedItems()
{
    for (int i = 0; i < m_Items.size();) {
        if(!m_Items[i].done) {
              i;
            continue;
        }
        //otherwise...
        emit preItemRemoved(i);

        m_Items.removeAt(i);

        emit postItemRemoved();
    }
}


###################################################################

The c   files implementing the listmodel: 
todomodel.h and todomodel.cpp
==================================================

#ifndef TODOMODEL_H
#define TODOMODEL_H

#include <QAbstractListModel>

class ToDoList;

class TodoModel : public QAbstractListModel
{
    Q_OBJECT
    Q_PROPERTY(ToDoList *list READ list WRITE setList)

public:
    explicit TodoModel(QObject *parent = nullptr);
    enum {
        DoneRole = Qt::UserRole,
        DescriptionRole
    };

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

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

    // Editable:
    bool setData(const QModelIndex &index, const QVariant &value,
                 int role = Qt::EditRole) override;

    Qt::ItemFlags flags(const QModelIndex& index) const override;

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

    ToDoList *list() const;
    void setList(ToDoList *newList);

private:
    ToDoList *m_List;
};

#endif // TODOMODEL_H


===================================================================


#include "todomodel.h"
#include "todolist.h"

TodoModel::TodoModel(QObject *parent)
    : QAbstractListModel(parent)
    , m_List(nullptr)
{
}

int TodoModel::rowCount(const QModelIndex &parent) const
{
    // For list models only the root node (an invalid parent) should return the list's size. For all
    // other (valid) parents, rowCount() should return 0 so that it does not become a tree model.
    if (parent.isValid() || !m_List)
        return 0;

    return m_List->items().size();
}

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

    const ToDoItem item = m_List->items().at(index.row());
    switch (role) {
    case DoneRole:
       return QVariant(item.done);

    case DescriptionRole:
       return QVariant(item.description);
    }

    return QVariant();
}

bool TodoModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
    if (!m_List) {
        return false;
    }

    ToDoItem item = m_List->items().at(index.row());
    switch (role) {
    case DoneRole:
       item.done = value.toBool();
        break;
    case DescriptionRole:
       item.description = value.toByteArray();
        break;
    }

    if (m_List->setItemAt(index.row(), item)) {
        emit dataChanged(index, index, QVector<int>() << role);
        return true;
    }
    return false;
}

Qt::ItemFlags TodoModel::flags(const QModelIndex &index) const
{
    if (!index.isValid())
        return Qt::NoItemFlags;

    return Qt::ItemIsEditable;
}

QHash<int, QByteArray> TodoModel::roleNames() const
{
    QHash<int, QByteArray> names;
    names[DoneRole] = "done";
    names[DescriptionRole] = "description";

    return names;
}

ToDoList *TodoModel::list() const
{
    return m_List;
}

void TodoModel::setList(ToDoList *newList)
{
    beginResetModel();

    if(m_List) {
        m_List->disconnect();
    }

    m_List = newList;

    if(!m_List) {
        endResetModel();
        return;
    }

    connect(m_List, &ToDoList::preItemAppended, this, [=]() {
        const int index = m_List->items().size();
        beginInsertRows(QModelIndex(), index, index);
    });
    connect(m_List, &ToDoList::postItemAppended, this, [=]() {
        endInsertRows();
    });

    connect(m_List, &ToDoList::preItemRemoved, this, [=](int index) {
        beginRemoveRows(QModelIndex(), index, index);
    });
    connect(m_List, &ToDoList::postItemRemoved, this, [=]() {
        endRemoveRows();
    });

    endResetModel();
}
#########################################################################

The View File: ToDoList.qml
Displays the data provided by the c   classes
============================================

import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.3

import ToDo 1.0

ColumnLayout {
    Frame {
        Layout.fillWidth: true

        ListView {
            implicitWidth: 250
            implicitHeight: 250
            clip: true
            anchors.fill: parent

            model: TodoModel {
                list: toDoList
            }

            delegate: RowLayout {
                width: parent.width

                CheckBox {
                    checked: model.done
                    onClicked: model.done=checked
                }
                TextField {
                    text: model.description
                    onEditingFinished: model.desciption = text
                    Layout.fillWidth: true
                }
            }
        }

    }

    RowLayout {
        Button {
            text: qsTr("Add new item")
            onClicked: toDoList.appendItem()
            Layout.fillWidth: true
        }

        Button {
            text: qsTr("Remove Completed Items")
            onClicked: toDoList.removeCompletedItems()
            Layout.fillWidth: true
        }
    }
}






CodePudding user response:

Whenever the Model changes we should notify the View. Refer to this link:

QML views are automatically updated when the model changes. Remember the model must follow the standard rules for model changes and notify the view when the model has changed by using QAbstractItemModel::dataChanged(), QAbstractItemModel::beginInsertRows(), and so on. See the Model subclassing reference for more information.

Here's how you can enhance your example to achieve a similar result:
On click of Fetch data button, after 3 seconds, the first row's description changes to https.

todolist.h:

signals:
    void updateData();
public slots:
    void fetchData();

todolist.cpp:

void ToDoList::fetchData()
{
    QTimer::singleShot(3000, (QObject*)this, SIGNAL(updateData()));
}

todomodel.cpp:

connect(mList, &ToDoList::postItemRemoved, this, [=]() {
    endRemoveRows();
});

connect(mList, &ToDoList::updateData, this, [=]() {
    QVariant value = "https";
    QModelIndex index = createIndex(0,0);
    setData(index, value, DescriptionRole);
});

ToDoList.qml:

Button {
    text: qsTr("Remove completed")
    onClicked: toDoList.removeCompletedItems()
    Layout.fillWidth: true
}
Button {
    text: qsTr("Fetch data")
    onClicked: toDoList.fetchData()
    Layout.fillWidth: true
}

CodePudding user response:

I have now fixed my issue thanks to @ArunKumarB. The main tip from @ArunKumarB's comment was QModelIndex index = createIndex(0,0); That is, how to convert a row index to a QModelIndex object. The rest was mostly plumbing.

  •  Tags:  
  • Related