Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

QAbstractListModel dataChanged signal not updating ListView (QML)

I have a QAbstractListModel connected to a ListView in QML, but I'm having an issue with updating the view from C++. This is with Qt 5.6 mingw, QtQuick 2.6, and QtQuick.Controls 1.5.

Setup: The ListView uses a custom check box delegate with a property to store the value from the model. The delegate updates the model when a user clicks on the delegate. In my QML I also have a toggle button that calls a slot in my model which toggles the data in the model and emits the dataChanged() signal for all rows (sets all checkboxes to not checked or checked).

Issue: The toggle button works just fine until a user interacts with any checkbox delegate. After a user does this, the dataChanged() signal no longer updates that specific delegate. I also verified that the data() function of my model is getting called for all rows before the user interaction and then it is only getting called on the rows the user didn't click after the user interaction. This leads me to believe somewhere behind the scenes the view is choosing not to update certain rows, but I can't figure out why.

Possible Solution: Emitting layoutChanged() in the model does update the view for my toggle button regardless of user interaction, but this causes the entire view to be redrawn and is relatively slow.

This issue was created using a checkbox, but it applies to any type of user interaction. Below is all of the code necessary to recreate my issue.

main.qml

import QtQuick 2.6
import QtQuick.Controls 1.5
import QtQuick.Layouts 1.3

ApplicationWindow {
    visible: true
    width: 640
    height: 480

    ColumnLayout {
        anchors.fill: parent
        spacing: 10

        Button {
            Layout.preferredHeight: 100
            Layout.preferredWidth: 100
            text: "Test!"
            onClicked: {
                console.log("attempting to refresh qml")
                testModel.refresh()
                testModel.print()
            }
        }


        ScrollView {
            Layout.fillHeight: true
            Layout.fillWidth: true

            ListView {
                id: view
                anchors.fill: parent
                spacing: 5
                model: testModel
                delegate: Rectangle {

                    height: 50
                    width: 100
                    color: "lightgray"

                    CheckBox {
                        id: checkBox
                        anchors.fill: parent
                        checked: valueRole

                        onClicked: {
                            valueRole = checked
                        }
                    }
                }
            }
        }
    }
}

TestModel.cpp

#include "testmodel.h"

TestModel::TestModel(QObject *parent) : QAbstractListModel(parent) {

    roleVector << TaskRoles::valueRole;
    testValue = false;
}

TestModel::~TestModel() {}

void TestModel::setup(const QList<bool> &inputList) {

    // Clear view
    removeRows(0, valueList.length());

    // Update value list
    valueList = inputList;

    // Add rows
    insertRows(0, valueList.length());
}

// Emits data changed for entire model
void TestModel::refresh() {

    qDebug() << "attempting to refresh c++";

    // Toggle all values in model
    for (int i=0; i < valueList.length(); i++) {
        valueList[i] = testValue;
    }

    // Toggle test value
    testValue = !testValue;

    // Update view
    // this works but is slow
//    layoutAboutToBeChanged();
//    layoutChanged();

    // this doesn't work if the user clicked the checkbox already
    dataChanged(createIndex(0, 0), createIndex(rowCount()-1, 0), roleVector);
}

void TestModel::print() {
    qDebug() << "Model:" << valueList;
}

QHash<int, QByteArray> TestModel::roleNames() const {

    QHash<int, QByteArray> roles;
    roles[valueRole]         = "valueRole";
    return roles;
}

int TestModel::rowCount(const QModelIndex & /*parent*/) const {

    return valueList.length();
}

QVariant TestModel::headerData(int /*section*/, Qt::Orientation /*orientation*/, int /*role*/) const {

    return QVariant();
}

Qt::ItemFlags TestModel::flags(const QModelIndex & /*index*/) const {

    return static_cast<Qt::ItemFlags>(Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable |  Qt::ItemIsEditable);
}

QVariant TestModel::data(const QModelIndex &index, int role) const {

//    qDebug() << QString("Get Data - Row: %1, Col: %2, Role: %3").arg(index.row()).arg(index.column()).arg(role);

    int row = index.row();

    if (row >= 0 && row < valueList.length()) {

        switch(role) {
            case valueRole:
            qDebug() << QString("Get Data - Row: %1, Col: %2, Role: %3").arg(index.row()).arg(index.column()).arg(role);
                return valueList.at(row);
            default:
                return QVariant();
        }
    }

    return QVariant();
}

bool TestModel::setData(const QModelIndex &index, const QVariant &value, int role) {

    qDebug() << QString("Set Data - Row: %1, Col: %2, Role: %3").arg(index.row()).arg(index.column()).arg(role);

    int row = index.row();

    if (row >= 0 && row < valueList.length()) {

        switch(role) {
            case valueRole:
                valueList[row] = value.toBool();
                break;
            default:
                break;
        }

        dataChanged(index, index, QVector<int>() << role);
        print();
    }

    return true;
}

bool TestModel::insertRows(int row, int count, const QModelIndex & /*parent*/) {

    // Check bounds
    if (row < 0 || count < 0) {
        return false;
    }

    if (count == 0) {
        return true;
    }

    if (row > rowCount()) {
        row = rowCount();
    }

    beginInsertRows(QModelIndex(), row, row+count-1);
    endInsertRows();

    return true;
}

bool TestModel::removeRows(int row, int count, const QModelIndex & /*parent*/) {

    // Check bounds
    if (row < 0 || count < 0 || rowCount() <= 0) {
        return false;
    }

    if (count == 0) {
        return true;
    }

    if (row >= rowCount()) {
        row = rowCount() - 1;
    }

    beginRemoveRows(QModelIndex(), row, row+count-1);
    endRemoveRows();

    return true;
}

TestModel.h

#ifndef TESTMODEL_H
#define TESTMODEL_H

#include <QAbstractListModel>
#include <QDebug>
#include <QVector>


class TestModel : public QAbstractListModel
{
    Q_OBJECT
public:
    explicit TestModel(QObject *parent = 0);
    ~TestModel();

    // Roles
    enum TaskRoles {
        valueRole = Qt::UserRole + 1,
    };

    // Row / Column Functions
    int rowCount(const QModelIndex &parent = QModelIndex()) const ;

    // Header / Flag Functions
    QVariant headerData(int section, Qt::Orientation orientation, int role) const;
    Qt::ItemFlags flags(const QModelIndex &index) const;

    // Model Get / Set Functions
    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
    bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole);

    // Row Insertion / Deletion Functions
    bool insertRows(int row, int count, const QModelIndex &parent = QModelIndex());
    bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex());

protected:
    // Value List
    QList<bool> valueList;
    QVector<int> roleVector;
public slots:
    QHash<int, QByteArray> roleNames() const;
    void setup(const QList<bool> &inputList);
    void refresh();
    void print();
};

#endif // TESTMODEL_H

main.cpp

#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QtQml>
#include <QQmlContext>

#include "testmodel.h"

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);

    TestModel testModel;
    testModel.setup(QList<bool>() << true << false << true << false << true);

    QQmlApplicationEngine engine;
    engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
    engine.rootContext()->setContextProperty("testModel", &testModel);

    return app.exec();
}

Example debug output (notice how it goes from 5 get data calls to only 4 get data calls after the checkbox in the delegate is clicked):

qml: attempting to refresh qml
attempting to refresh c++
"Get Data - Row: 0, Col: 0, Role: 257"
"Get Data - Row: 1, Col: 0, Role: 257"
"Get Data - Row: 2, Col: 0, Role: 257"
"Get Data - Row: 3, Col: 0, Role: 257"
"Get Data - Row: 4, Col: 0, Role: 257"
Model: (false, false, false, false, false)

qml: clicked checkbox
"Set Data - Row: 0, Col: 0, Role: 257"
Model: (true, false, false, false, false)

qml: attempting to refresh qml
attempting to refresh c++
"Get Data - Row: 1, Col: 0, Role: 257"
"Get Data - Row: 2, Col: 0, Role: 257"
"Get Data - Row: 3, Col: 0, Role: 257"
"Get Data - Row: 4, Col: 0, Role: 257"
Model: (true, true, true, true, true)

qml: attempting to refresh qml
attempting to refresh c++
"Get Data - Row: 1, Col: 0, Role: 257"
"Get Data - Row: 2, Col: 0, Role: 257"
"Get Data - Row: 3, Col: 0, Role: 257"
"Get Data - Row: 4, Col: 0, Role: 257"
Model: (false, false, false, false, false)
like image 896
Frank Laritz Avatar asked Mar 12 '23 03:03

Frank Laritz


2 Answers

Modify your delegate to something like this:

delegate: Rectangle {

    height: 50
    width: 100
    color: "lightgray"

    CheckBox {
        id: checkBox
        anchors.fill: parent
        checked: valueRole

//      onClicked: {
//          valueRole = checked
//      }
    }

    MouseArea {
        anchors.fill: parent
        onClicked: {
            valueRole = !checkBox.checked
        }
    }
}

Checkbox's value is bind to valueRole but it just overwrites this binding with bool value when it is clicked. If you handle the click some other way, for example by covering Checkbox with MouseArea you will omit breaking the binding and everything will be working.

like image 97
Filip Hazubski Avatar answered Apr 06 '23 13:04

Filip Hazubski


Another option is to restore the binding.

CheckBox {
    onClicked: {
        valueRole = checked;
        checked = Qt.binding(function() { return valueRole; });
    }
}

The advantage of this approach is that it also works if the user interaction is not a simple click, i.e. if the user interaction can not simply be intercepted with a MouseArea.

like image 26
Kevin Krammer Avatar answered Apr 06 '23 13:04

Kevin Krammer