Frank Laritz Frank Laritz - 2 months ago 33
C++ Question

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)

Answer

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.

Comments