【问题标题】:How to delete row from TableView properly如何正确从 TableView 中删除行
【发布时间】:2021-03-27 09:41:30
【问题描述】:

我在 Manjaro 上使用 Qt 5.15.0。

我有一个在 qml 中使用 TableView 创建的表。

我可以通过右键单击 Id 字段来删除一行,如下所示:

删除等工作,但我在 qml 中收到这些错误:

删除行的方法在main.qml中是这样的:

function deleteRowFromDatabase(row) {
       console.log("before" + model.countOfRows())

       if (!model.removeEntry(row)) {
           console.log(qsTr("remove row %1 failed").arg(row))
       }

       model = QuestionsProxyModel
       console.log("after" + model.countOfRows())
   }

错误指向main.qml中id的delegate行

        DelegateChoice {
            column: 0
            delegate: QuestionIdDelegate {
                id: questionIdDelegate
                width: tableView.columnWidthProvider(column)
                text: model.id                    /// this is undefined
                row: model.row

                Component.onCompleted: {
                    questionIdDelegate.markForDelete.connect(
                                tableView.deleteRowFromDatabase)
                }
            }
        }

行的删除是从 C++ 在派生自 questionsproxmodel.h 中的 QIdentityProxyModel 的类中实现的:

bool QuestionsProxyModel::removeEntry(int row)
{
    return removeRows(row, 1);
}

此模型采用派生自 QSqlTableModel 的类 QuestionSqlTableModel 作为源模型

删除行在questionssqltablemodel.qml中是这样实现的:

     bool QuestionSqlTableModel::removeRows(int row, int count,
                                               const QModelIndex &parent)
        {
            auto result = QSqlTableModel::removeRows(row, count, parent);
            if (result) {
                select(); // row is not deleted from sql database until select is called
            }
        
            return result;
        }

据我了解,模型的 countOfRows 仅在调用 select() 后才会更新,因此我假设在 QSqlTableModel::removeRowsselect 之间,TableView 会再读取一次不存在的行并在 QML 中导致这些错误。如何预防?

完整源代码试用:

ma​​in.cpp:

#include <QDebug>
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQuickStyle>

#include <QFile>
#include <QSqlDatabase>
#include <QSqlQuery>

#include <QSqlError>

#include "questionsproxymodel.h"
#include "questionsqltablemodel.h"

int main(int argc, char *argv[])
{
    QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);

    QGuiApplication app(argc, argv);

    QUrl dbUrl{"file:///home/sandro/Desktop/test.db"};
    auto exists = QFile::exists(dbUrl.toLocalFile());

    auto db = QSqlDatabase::addDatabase("QSQLITE", "DBConnection");
    db.setDatabaseName(dbUrl.toLocalFile());
    db.open();

    if (!exists) {
        const QString questionTableName = "questions";
        QSqlQuery query{db};
        query.exec("CREATE TABLE " + questionTableName +
                   " ("
                   "id INTEGER PRIMARY KEY AUTOINCREMENT)");
    }

    QScopedPointer<QuestionSqlTableModel> questionSqlTableModel(
        new QuestionSqlTableModel(nullptr, db));

    QScopedPointer<QuestionsProxyModel> questionsProxyModel{
        new QuestionsProxyModel};
    questionsProxyModel->setSourceModel(questionSqlTableModel.get());

    if (!exists) {
        for (int i = 0; i < 10; ++i) {
            questionsProxyModel->addNewEntry();
        }
    }

    QQmlApplicationEngine engine;

    qmlRegisterSingletonInstance<QuestionsProxyModel>(
        "QuestionsProxyModels", 1, 0, "QuestionsProxyModel",
        questionsProxyModel.get());

    const QUrl url(QStringLiteral("qrc:/qml/main.qml"));
    engine.load(url);

    return app.exec();
}

questionssqltablemodel.h

#include <QSqlTableModel>

class QuestionSqlTableModel : public QSqlTableModel {
    Q_OBJECT
public:
    explicit QuestionSqlTableModel(QObject *parent = nullptr,
                                   const QSqlDatabase &db = QSqlDatabase());

    bool removeRows(int row, int count, const QModelIndex &parent) override;
};

问题sqltablemodel.cpp

#include "questionsqltablemodel.h"

#include <QBuffer>
#include <QDebug>
#include <QPixmap>

#include <QSqlError>
#include <QSqlField>
#include <QSqlRecord>
#include <QSqlRelationalDelegate>

QuestionSqlTableModel::QuestionSqlTableModel(QObject *parent,
                                             const QSqlDatabase &db)
    : QSqlTableModel{parent, db}
{
    setTable("questions");
    setSort(0, Qt::AscendingOrder);
    if (!select()) {
        qDebug() << "QuestionSqlTableModel: Select table questions failed";
    }
    setEditStrategy(EditStrategy::OnFieldChange);
}

bool QuestionSqlTableModel::removeRows(int row, int count,
                                       const QModelIndex &parent)
{
    auto result = QSqlTableModel::removeRows(row, count, parent);
    if (result) {
        select(); // row is not deleted from sql database until select is called
    }

    return result;
}

questionsproxymodel.h:

#include <QIdentityProxyModel>
#include <QObject>

class QuestionsProxyModel : public QIdentityProxyModel {
    Q_OBJECT

    enum questionRoles {
        idRole = Qt::UserRole + 1,
    };

public:
    QuestionsProxyModel(QObject *parent = nullptr);

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

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

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

    bool addNewEntry();
    Q_INVOKABLE bool removeEntry(int row);
    Q_INVOKABLE int countOfRows() const;

private:
    QModelIndex mapIndex(const QModelIndex &source, int role) const;
};

questionsproxymodel.h:

#include "questionsproxymodel.h"

#include <QBuffer>
#include <QDebug>
#include <QPixmap>

#include <QByteArray>

#include <QSqlError>
#include <QSqlTableModel>

QuestionsProxyModel::QuestionsProxyModel(QObject *parent)
    : QIdentityProxyModel(parent)
{
}

QHash<int, QByteArray> QuestionsProxyModel::roleNames() const
{
    QHash<int, QByteArray> roles;
    roles[idRole] = "id";
    return roles;
}

QVariant QuestionsProxyModel::data(const QModelIndex &index, int role) const
{
    QModelIndex newIndex = mapIndex(index, role);
    if (role == idRole) {

        return QIdentityProxyModel::data(newIndex, Qt::DisplayRole);
    }
    return QIdentityProxyModel::data(newIndex, role);
}

bool QuestionsProxyModel::setData(const QModelIndex &index,
                                  const QVariant &value, int role)
{
    QModelIndex newIndex = mapIndex(index, role);

    if (role == idRole) {

        return QIdentityProxyModel::setData(newIndex, value, Qt::EditRole);
    }
    return QIdentityProxyModel::setData(newIndex, value, role);
}

bool QuestionsProxyModel::addNewEntry()
{
    auto newRow = rowCount();

    if (!insertRows(newRow, 1)) {
        return false;
    }
    if (!setData(createIndex(newRow, 0), newRow + 1)) {
        removeRows(newRow, 1);
        return false;
    }
    auto sqlModel = qobject_cast<QSqlTableModel *>(sourceModel());
    return sqlModel->submit();
}

bool QuestionsProxyModel::removeEntry(int row)
{
    return removeRows(row, 1);
}

int QuestionsProxyModel::countOfRows() const
{
    return rowCount();
}

QModelIndex QuestionsProxyModel::mapIndex(const QModelIndex &source,
                                          int role) const
{
    switch (role) {
    case idRole:
        return createIndex(source.row(), 0);
    }
    return source;
}

ma​​in.qml

import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Window 2.15
import Qt.labs.qmlmodels 1.0
import QtQuick.Controls.Material 2.15

import QuestionsProxyModels 1.0

ApplicationWindow {
    id: root
    visible: true
    width: 1460
    height: 800

    TableView {
        id: tableView
        width: parent.width

        anchors.fill: parent
        boundsBehavior: Flickable.StopAtBounds

        reuseItems: true
        clip: true
        property var columnWidths: [60]
        columnWidthProvider: function (column) {
            return columnWidths[column]
        }

        model: QuestionsProxyModel

        delegate: DelegateChooser {
            id: chooser

            DelegateChoice {
                column: 0
                delegate: QuestionIdDelegate {
                    id: questionIdDelegate
                    width: tableView.columnWidthProvider(column)
                    text: model.id
                    row: model.row

                    Component.onCompleted: {
                        questionIdDelegate.markForDelete.connect(
                                    tableView.deleteRowFromDatabase)
                    }
                }
            }
        }
        ScrollBar.vertical: ScrollBar {}

        function deleteRowFromDatabase(row) {
            console.log("before" + model.countOfRows())

            if (!model.removeEntry(row)) {
                console.log(qsTr("remove row %1 failed").arg(row))
            }

            model = QuestionsProxyModel
            console.log("after" + model.countOfRows())
        }
    }
}

QuestionIdDelegate.qml:

import QtQuick 2.15
import QtQuick.Controls 2.15

TextField {
    property int row

    signal markForDelete(int row)

    id: root

    implicitHeight: 100

    horizontalAlignment: Text.AlignHCenter
    verticalAlignment: Text.AlignVCenter

    readOnly: true

    background: Frame {}

    MouseArea {
        id: mouseArea
        anchors.fill: parent
        acceptedButtons: Qt.RightButton

        onClicked: {
            eraseContextMenu.popup(root, 0, mouseArea.mouseY + 10)
        }
    }

    Menu {
        id: eraseContextMenu
        y: root.y
        MenuItem {
            text: qsTr("Delete entry")
            onTriggered: {
                eraseDialog.open()
                eraseContextMenu.close()
            }
        }
        MenuItem {
            text: qsTr("Cancel")
            onTriggered: {
                eraseContextMenu.close()
            }
        }
    }

    Dialog {
        id: eraseDialog
        title: qsTr("Delete database entry")
        modal: true
        focus: true

        contentItem: Label {
            id: label
            text: qsTr("Do you really want to erase the entry with id %1?").arg(
                      root.text)
        }

        onAccepted: {
            markForDelete(root.row)
        }

        standardButtons: Dialog.Ok | Dialog.Cancel
    }
}

.pro:

QT += quick
QT += quickcontrols2
QT += sql

CONFIG += c++17

DEFINES += QT_DEPRECATED_WARNINGS

HEADERS += \
    questionsproxymodel.h \
    questionsqltablemodel.h

SOURCES += \
        main.cpp \
        questionsproxymodel.cpp \
        questionsqltablemodel.cpp

RESOURCES += qml.qrc

# Additional import path used to resolve QML modules just for Qt Quick Designer
QML_DESIGNER_IMPORT_PATH =

# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target

编辑:

答案解决了上述错误。但是我检测到代码的另一个问题。

如果我删除第 1 行,然后删除第 2 行,我的输出如下所示:

这个绑定循环错误指向Dialog in QuestionsIDDelegate

Dialog {
    id: eraseDialog
    title: qsTr("Delete database entry")
    modal: true
    focus: true

    contentItem: Label {
        id: label
        text: qsTr("Do you really want to erase the entry with id %1?").arg(
                  root.text)
    }

    onAccepted: {
        markForDelete(root.row)
    }

    standardButtons: Dialog.Ok | Dialog.Cancel
}

【问题讨论】:

  • 我将其缩短为最小示例。由于 sql 背景,它仍然有点笨重
  • 您使用的角色名称为id。我不确定这是否真的会导致您的应用出现问题,但至少,我认为它不安全,因为它是内置的 QML 属性。我很想知道您是否只需更改名称就能获得更好的结果?
  • 这是一个很好的观点。我想我会重命名该列。但是在我的实际应用程序中,我有更多未命名为 id 的列发生相同的分配错误。

标签: c++ qt qml


【解决方案1】:

防止这种情况的一种方法是检查该属性是否未定义:

text: model.id === undefined ? "" : model.id

【讨论】:

  • 非常感谢确实解决了错误。你也可以看看绑定循环错误。哪个也显示在代码中?这种行为是否也意味着 QML TableView 和模型暂时不同步?
【解决方案2】:

我不会修复代码,但我会尝试解释为什么您遇到问题。我觉得事情的原因会帮助更多的人。

此图像可能不是 100% 准确,但这是人们必须在脑海中想象事物的方式。引入 QML 时,对象存在三个不同的通道:C++、QML 引擎和 JavaScript 引擎。这些车道中的每一个都毫无疑问地相信他们控制着对象的生死

当您只传递按值传递的整数时,这不是问题。当您传递 QStrings 时,由于 Qt 的写时复制章程,这只是一个小问题。当您传递真实对象时,更糟糕的是,真实对象的复杂容器,您必须完全理解这一点。 “解决”您的问题的答案实际上只是掩盖了它。你会发现有很多 QML 和 JavaScript 代码,它们的存在只是为了掩盖不尊重车道的应用程序。

如果这个应用程序使用了一个实际的数据库,就会有第四条通道。是的,SQLite 提供了一个 SQL 接口并允许做很多事情,但是一个实际的数据库有一个外部引擎,提供共享访问和控制游标的生命周期。 SQLite 文件往往是单用户的。是的,您的应用程序中的多个线程可以访问它,但是当您的应用程序运行时,您不能打开终端窗口并使用命令行工具来检查数据库。将其更多地视为一个非常好的索引文件系统,无需共享。

因此,您在 C++ 中创建一个对象,然后将其公开给 QML。 QML 引擎现在毫无疑问地相信,尽管没有自己的副本,但它控制着该对象的生死。

QML 真的很弱。它实际上并不能做太多事情,所以它必须将任何重要的对象交给 JavaScript。 JavaScript 引擎现在毫无疑问地相信它现在控制着该对象的生死。

您还需要将这些通道设想为独立线程。每个线程中很可能有很多线程,但一般来说,这些线程之间的任何信号或通信都将在目标的事件循环中进行作为排队事件。这意味着它只有在最终冒泡到该事件循环的队列顶部时才会被处理。

顺便说一句,这就是为什么在使用 QML/JavaScript 时永远不能使用智能指针。尤其是在没有更多引用时进行引用计数并删除对象的那种。 C++ 通道无法知道 QML 或 JavaScript 仍在使用该对象。

告诉您检查未定义属性的答案是掩盖您的代码在所有车道上酒驾的问题。最终,在更快(或有时更慢)的处理器上,其中一条通道的垃圾收集将在不合时宜的时刻运行,您将看到堆栈转储。 (酒驾代码会撞到一棵不屈服的树。)

正确的解决方案 #1: 永远不要使用 QML 或 JavaScript。只需使用 C++ 和小部件。完全留在 C++ 车道内。如果那是一条对您开放的路线,那将是一个不错的选择。有大量的生产代码就是这样做的。您可以获取一份this book 的副本(或直接从页面下载源代码)并糊涂构建它。

正确的解决方案 #2: 在 QML 或 JavaScript 中从不真正做任何事情。这是一个与#1 完全不同的解决方案。您可以仅将 QML 用于 UI,将所有逻辑留在 C++ 中。您的代码失败了,因为您正在尝试实际做某事。我还没有构建或尝试过你的代码。我只是看到了

function deleteRowFromDatabase(row)

根本不应该存在。 C++ 持有你的模型。当用户操作需要删除时,您会从 QML/JavaScript 通道发出信号。该信号成为 C++ 通道中的排队事件。当它被处理时,该行将被删除并更新模型。如果您已将模型正确地暴露给 QML,它将发出某种形式的“模型更改”信号,并且 UI 将相应更新。 MVC(Model-View-Controler)的要点之一是与模型之间的通信。当数据发生变化时,它会通知视图。

正确的解决方案 #3: 永远不要使用 C++。让您的 C++ 成为“Hello World!”刚刚启动 QML 的 shell。永远不要在 C++ 和其他两个通道之间创建和共享对象。在 JavaScript 和 QML 内完成所有工作。

绑定循环错误通常发生在代码酒后驾车穿越所有三个车道时。当代码没有将每个通道视为至少一个具有自己事件循环的不同线程时,它们也会发生。 C++ 通道尚未完成(可能甚至没有开始)删除操作,但您的代码已经在尝试使用结果。

我不知道为什么,但有些人会编辑这些内容并删除正确的答案。完整的信息对于 SO 来说太长了,包含在这里,

https://www.logikalsolutions.com/wordpress/uncategorized/so-you-cant-get-your-qt-models-to-work-with-qml/

甚至有源代码。

【讨论】:

  • 我知道解决方案 1。使用解决方案 2,您建议我将 QML 的信号连接到 C++ 中的模型。所以它会在正确的时间被调用?
  • 没有。您无法控制车道之间任何事物的时间。那是接触 QML 和 JavaScript 的人所不理解的。使用它的唯一正确方法是根本不使用它。这个函数消失了:function deleteRowFromDatabase(row) { console.log("before" + model.countOfRows()) if (!model.removeEntry(row)) { console.log(qsTr("remove row %1 failed") .arg(row)) } model = QuestionsProxyModel console.log("after" + model.countOfRows()) } 你向 C++ 发送一个信号,就是这样。
  • 您正在尝试实际“使用”QML 和 JavaScript,但这是做不到的。您正在尝试使用自己的线程和 eventLoop 控制不同通道中的执行顺序。这是无法做到的。更糟糕的是,代码试图在之后立即重用模型。您只需将信号发送到 C++。当模型更新后,它应该会自动发出其他车道数据已更改的信号。无需您在 QML/JavaScript 中接触模型,这一切都应该继续愉快地进行。最坏的情况是,当 C++ 通道完成强制更新时会发送一个信号。应该不需要。
  • 好的,我想我明白了。我应该从 QML 发出一个信号,而不是直接从 javascript 调用擦除,这会调用 C++ 站点上的 Slot。这样,它就会在正确的时间被调用。谢谢你的解释。
  • 我试过了,即使向 c++ 站点发送信号并从 C++ 中删除仍然会出现两个错误,我仍然需要从另一个答案中修复。对于发布的代码,我真的很感激这里的解决方案。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-03-17
  • 2017-03-04
  • 1970-01-01
  • 1970-01-01
  • 2014-04-29
  • 2013-08-15
  • 1970-01-01
相关资源
最近更新 更多