Как правильно удалить строку из TableView

Я использую Qt 5.15.0 на Manjaro.

У меня есть таблица, которую я создал с помощью TableView в qml.

Я могу удалить строку, щелкнув правой кнопкой мыши поле Id следующим образом:

Как правильно удалить строку из TableView

Удаление и т. д. работает, но я получаю эти ошибки в qml:

Как правильно удалить строку из TableView

Метод удаления строк выглядит следующим образом в main.qml:

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

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

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

Ошибка указывает на строку идентификатора делегата в main.qml

        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++ в классе, производном от QIdentityProxyModel в questionsproxmodel.h:

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

Эта модель использует класс QuestionSqlTableModel, производный от QSqlTableModel, в качестве исходной модели.

Удаление строк реализовано следующим образом в 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::removeRows и select TableView считывает еще раз несуществующую строку и вызывает эти ошибки в QML. Как это можно предотвратить?

Полный исходный код, чтобы попробовать:

основной.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();
}

вопросыsqltablemodel.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;
}

вопросыproxymodel.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;
};

вопросыproxymodel.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;
}

main.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.info("before" + model.countOfRows())

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

            model = QuestionsProxyModel
            console.info("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
    }
}

.про:

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 на 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

Sandro4912 17.12.2020 17:51

Вы используете имя роли id. Я не знаю точно, вызывает ли это проблему в вашем приложении, но, по крайней мере, я считаю это небезопасным, поскольку это встроенное свойство QML. Мне было бы любопытно узнать, получите ли вы лучшие результаты, просто изменив имя?

JarMan 18.12.2020 22:38

это хороший момент. Думаю, я переименую этот столбец. Однако в моем реальном приложении у меня есть больше столбцов, которые не имеют имени id с той же ошибкой назначения.

Sandro4912 19.12.2020 14:51
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
3
716
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Один из способов предотвратить это — проверить, что свойство не является неопределенным:

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

Большое спасибо, это действительно решает ошибку. Можете ли вы также взглянуть на ошибку цикла привязки. Что также отображается с кодом? Также означает ли поведение, что QML TableView и модель на мгновение не синхронизированы?

Sandro4912 19.12.2020 14:41

Я не собираюсь исправлять код, но попытаюсь объяснить, почему у вас возникают проблемы. Я чувствую, почему вещи помогут большему количеству людей.

Это изображение может быть не на 100% точным, но именно так нужно визуализировать вещи в своем уме. Когда вы представляете QML, у вас есть три различных направления, в которых живет объект: C++, QML Engine и JavaScript engine. Каждый из этих переулков без тени сомнения верит, что они контролируют жизнь и смерть объекта.

Когда вы просто передаете целые числа, которые передаются по значению, это не проблема. Когда вы передаете QString, из-за устава Qt на копирование при записи, это незначительная проблема. Когда вы передаете реальные объекты, что еще хуже, сложные контейнеры с реальными объектами, вы должны полностью это понимать. Ответ, который «решил» вашу проблему, на самом деле только замаскировал ее. Вы обнаружите, что существует много кода QML и JavaScript, который существует только для маскировки приложения, не соблюдающего правила.

Если бы это приложение использовало реальную базу данных, была бы четвертая полоса. Да, SQLite предоставляет интерфейс SQL и позволяет многое, но реальная база данных имеет внешний механизм, обеспечивающий общий доступ и контролирующий жизнь курсоров. Файлы SQLite, как правило, предназначены для одного пользователя. Да, несколько потоков в вашем приложении могут получить к нему доступ, но пока ваше приложение работает, вы не можете открыть окно терминала и использовать инструменты командной строки для проверки базы данных. Думайте об этом больше как о действительно хорошей индексированной файловой системе без совместного использования.

Итак, вы создаете объект на C++, а затем предоставляете его QML. Движок QML теперь безоговорочно верит, что он управляет жизнью и смертью этого объекта, несмотря на то, что у него нет собственной копии.

QML действительно слаб. На самом деле он мало что может сделать, поэтому ему приходится передавать любой важный объект JavaScript. Движок JavaScript теперь верит без тени сомнения, что теперь он контролирует жизнь и смерть этого объекта.

Вам также необходимо представить эти дорожки как независимые потоки. Скорее всего, внутри каждого будет много потоков, но, как правило, любой сигнал или связь между ними будет проходить в цикле событий цели как событие в очереди. Это означает, что он будет обработан только тогда, когда он, наконец, окажется в верхней части очереди для этого цикла событий.

Вот, кстати, почему вы никогда не можете использовать интеллектуальные указатели при использовании QML/JavaScript. Особенно те, которые подсчитывают ссылки и удаляют объект, когда ссылок больше нет. Линия C++ не может узнать, что QML или JavaScript все еще используют объект.

Ответ, говорящий вам проверить неопределенное свойство, маскирует проблему, заключающуюся в том, что ваш код едет по всем полосам в нетрезвом виде. В конце концов, на более быстром (а иногда и более медленном) процессоре сборка мусора для одной из полос запустится в самый неподходящий момент, и вас встретит дамп стека. (Код вождения в нетрезвом виде ударит по неуступчивому дереву.)

Правильное решение №1: Никогда не используйте QML или JavaScript. Просто используйте C++ и виджеты. Оставайтесь полностью в полосе C++. Если это путь, открытый для вас, это хороший путь. Существует очень много производственного кода, делающего именно это. Вы можете получить копию этой книги (или просто скачать исходный код со страницы) и разобраться в ее создании.

Правильное решение №2: На самом деле никогда ничего не делайте в QML или JavaScript. Это все вместе другое решение, чем № 1. Вы можете использовать QML только для пользовательского интерфейса, оставив всю логику на C++. Ваш код не работает, потому что вы действительно пытаетесь что-то сделать. Я не создавал и не пробовал ваш код. я просто увидел

function deleteRowFromDatabase(row)

которого вообще не должно быть. С++ содержит вашу модель. Вы выдаете сигнал из своих дорожек QML/JavaScript, когда действие пользователя требует удаления. Этот сигнал становится событием в очереди на полосе C++. При обработке строка будет удалена, а модель обновлена. Если вы правильно отобразили свою модель в QML, она выдаст некоторую форму сигнала «модель изменена», и пользовательский интерфейс будет соответствующим образом обновлен. Одним из основных моментов MVC (Model-View-Controler) является связь с/от модели. Когда данные изменяются, он уведомляет представление (я).

Правильное решение №3: Никогда не используйте С++. Пусть ваш C++ будет "Hello World!" оболочка, которая просто запускает ваш QML. Никогда не создавайте и не делитесь объектом между 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++. Значит, он вызывается в нужное время?

Sandro4912 21.12.2020 14:36

Нет. Вы не можете контролировать время между дорожками. Это то, чего не понимают люди, тянущиеся к QML и JavaScript. Единственный правильный способ его использования — не использовать его вообще. Эта функция исчезает: function deleteRowFromDatabase(row) { console.info("before" + model.countOfRows()) if (!model.removeEntry(row)) { console.info(qsTr("удалить строку %1 не удалось") .arg(row)) } model = QuestionsProxyModel console.info("after" + model.countOfRows()) } Вы отправляете сигнал C++ и все.

user3450148 22.12.2020 15:51

Вы пытаетесь на самом деле «использовать» QML и JavaScript, но это просто невозможно. Вы пытаетесь контролировать порядок выполнения на другой полосе с собственным потоком и циклом обработки событий. Этого нельзя делать. Хуже того, код пытается повторно использовать модель сразу после этого. Вы просто отправляете сигнал на C++. Когда модель была обновлена, она должна автоматически сигнализировать об изменении других данных полосы движения. Все это должно продолжаться счастливо, даже если вы никогда не касаетесь модели в QML/JavaScript. В худшем случае у вас есть полоса C++, которая отправляет сигнал, когда это делается, чтобы принудительно обновить. Не должен быть нужен.

user3450148 22.12.2020 15:55

Хорошо, я думаю, я понимаю. Я должен выдать сигнал из QML вместо прямого вызова стирания из javascript, который вызывает слот на сайте C++. Таким образом, все это вызывается в нужное время. Спасибо за объяснение.

Sandro4912 22.12.2020 18:13

Я попробовал это, даже отправив сигнал на сайт С++ и удалив с С++, все равно возникают обе ошибки, и мне все еще нужно исправление из другого ответа. Я был бы очень признателен за решение здесь для опубликованного кода.

Sandro4912 23.12.2020 15:00

Вам нужно опубликовать код, где вы пытались сделать все на C++. Если вы получаете эту ошибку даже тогда, это потому, что вы: 1) все еще пытаетесь переназначить модель 2) делаете что-то еще неправильно

user3450148 23.12.2020 19:35

Вы не предоставили весь код. То, что вы предоставили, даже не скомпилируется. Отсутствуют операторы включения файлов. Какую версию Qt вы используете? Я спрашиваю, потому что, согласно этому сообщению: medium.com/@yuvaram.aligeti/… qmlRegisterSingletonInstance не является частью раннего Qt 5.x. Это определенно не является частью моей установки 5.12 на Ubuntu 2.04 LTS. Если вы хотите, чтобы люди физически отлаживали ваш код, вам необходимо предоставить полный ТОЧНЫЙ код, включая файл .PRO. Вы также должны быть конкретными в своих версиях. Если это 6.x, вы сами по себе.

user3450148 23.12.2020 20:25

Я добавил недостающий файл pro. Я не вижу, чтобы включаемые файлы отсутствовали. Я скопировал полный код, кроме включения защиты в файлы .h.

Sandro4912 24.12.2020 13:29

Questionsproxymodel.cpp (второй .h в его имени) даже не имеет оператора #include для questionsproxymodel.h. Несколько других отсутствуют такие вещи. Вы никогда не указывали, какую именно версию Qt вы используете. Код, который вы разместили здесь, НЕ БУДЕТ СОСТАВЛЯТЬСЯ в Ubuntu 20.04 LTS с версией Qt из репозиториев. qmlRegisterSingletonInstance не существует в этой версии. В этом нет абсолютно никаких причин использовать прокси-модель. Я закончил тратить время на это. Вы пытаетесь «исправить код», когда проблема заключается в плохой архитектуре.

user3450148 25.12.2020 17:24

вы правы, я пропустил, чтобы включить их. Без понятия, почему они пропали. Я использую Qt5.15.0 на Manjaro, и qmlRegisterSingletonInstance существует с тех пор, как он был представлен в Qt 5.14.

Sandro4912 25.12.2020 18:44

Другие вопросы по теме