QAbstractListModel dataChanged 信号不更新 ListView (QML)

QAbstractListModel dataChanged signal not updating ListView (QML)

我有一个 QAbstractListModel 连接到 QML 中的 ListView,但我在从 C++ 更新视图时遇到问题。这是 Qt 5.6 mingw、QtQuick 2.6 和 QtQuick.Controls 1.5.

设置: ListView 使用带有 属性 的自定义复选框委托来存储来自模型的值。当用户单击委托时,委托会更新模型。在我的 QML 中,我还有一个切换按钮,它在我的模型中调用一个插槽,它切换模型中的数据并为所有行发出 dataChanged() 信号(将所有复选框设置为未选中或选中)。

问题: 在用户与任何复选框委托进行交互之前,切换按钮工作正常。用户执行此操作后,dataChanged() 信号不再更新该特定委托。我还验证了我的模型的 data() 函数在用户交互之前被所有行调用,然后它只在用户交互后用户没有点击的行上被调用。这让我相信视图在幕后的某个地方选择不更新某些行,但我不明白为什么。

可能的解决方案: 无论用户交互如何,在模型中发出 layoutChanged() 都会更新我的切换按钮的视图,但这会导致重绘整个视图并且速度相对较慢。

此问题是使用复选框创建的,但它适用于任何类型的用户交互。以下是重现我的问题所需的所有代码。

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

示例调试输出(注意在单击委托中的复选框后如何从 5 次获取数据调用变为仅 4 次获取数据调用):

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)

将您的委托修改为如下内容:

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 的值绑定到 valueRole 但它只是在单击时用 bool 值覆盖此绑定。如果您以其他方式处理点击,例如用 MouseArea 覆盖 Checkbox,您将忽略打破绑定并且一切正常。

另一种选择是恢复绑定。

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

这种方法的优点是,如果用户交互不是简单的单击,即如果不能简单地用 MouseArea 拦截用户交互,它也适用。