LINUX.ORG.RU

Правильная работа с beginRemoveRows/endRemoveRows

 , ,


0

2

Привет, ЛОР.

Есть кутешная модель, унаследованная от QAbstractItemModel. Поставляет данные для дерева. Дошёл до удаления.

Насколько я понимаю:

  1. В принципе, все эти begin*Rows/end*Rows нужны исключительно для того, чтобы фрагментарно обновлять данные, не перерисовывая View целиком, и если данных немного, можно вместо них после изменения данных просто сделать модели refresh и не заморачиваться с индексами. (Правда, в случае дерева при этом схлопываются все узлы, открытые пользователем, что неудобно для последнего. С таблицей проще.)

  2. Если я всё-таки решил сделать хорошо и использовать beginRemoveRows… у него есть параметры first и last. Если массив индексов не сплошной (т.е. оператор выделил несколько произвольных узлов через Ctrl+ЛКМ), единственный путь – это вызывать пару beginRemoveRows/endRemoveRows для каждого удаляемого узла по очереди и между ними осуществлять удаление по одному узлу.

Я всё правильно понял?

P.S. Времена нынче трудные, многие ресурсы по Qt либо скончались (как некогда прекрасный prog.org.ru), либо недоступны из России. Из доступного есть, например, статья на сайте @Xintrea, но он работу с множеством записей не рассматривает, соответственно, у него везде просто first==last, и не дерево, а таблица (в случае дерева интересно заполнение первого параметра). Есть места, где можно поговорить по Qt, кроме ЛОРа? (Ну кроме ИИ, разумеется – он часто помогает в практических вопросах, но вот в труднопроверяемых концептуальных вещах всё-таки лучше говорить с людьми – галлюцинации никто не отменял.)

★★★★★

Последнее исправление: hobbit (всего исправлений: 3)
Ответ на: комментарий от CrazyAlex25

Да, кутешные examples я видел, там везде сплошной диапазон индексов, а родительский индекс пустой, формируется конструктором по умолчанию.

Ладно, с несплошным понятно, буду вызывать парами по одному. А для родительского индекса, видимо, по каждой записи придётся извлекать родительскую…

hobbit ★★★★★
() автор топика

Могу посоветовать посмотреть книги на русском по Qt.

Шлее М. - Qt 5.10. Профессиональное программирование на C++ (В подлиннике) - 2018
* Создание собственных моделей. Там есть про begin*Rows/end*Rows.

Жасмин Бланшет, Марк Саммерфилд - Qt 4. Программирование GUI на C++
Подобной информации в ней нет.

Боровский А. - Qt4.7+. Практическое программирование на C++ (2012)
Подобной информации в ней нет.

Марк Саммерфильд - Qt - Профессиональное программирование - Разработка кроссплатформенных приложений
* Деревья на основе модели QStandardItemModel
* Создание пользовательских древовидных моделей
* Изменение древовидной модели с помощью пользовательского интерфейса
* Специализированный подкласс QAbstractItemModel для деревьев

Xintrea ★★★★★
()
Ответ на: комментарий от Xintrea

Благодарю. Шлее читал, разумеется (более старое издание, правда), там этот вопрос примерно на том же уровне, что в examples. Бланшетта-Саммерфильда тоже.

…А вот четвёртую книгу из списка я, похоже, не видел.

hobbit ★★★★★
() автор топика

А доки по Qt у тебя есть?

либо недоступны из России

А Zeal работает? Там есть и Qt5, и Qt6.
Ну и в Kiwix есть Zim’ы со stackoverflow.

dataman ★★★★★
()
Последнее исправление: dataman (всего исправлений: 2)
Ответ на: комментарий от Xintrea

Благодарю.

В общем и целом там то же самое, но приведённые примеры напомнили мне о существовании index.parent(), поэтому реализация будет чуть проще, чем мне представлялось.

hobbit ★★★★★
() автор топика

Выяснилось, что вызов beginRemoveRows() уже инвалидирует переданный ему диапазон индексов, поэтому если по удаляемым записям надо собрать какую-то информацию (на основе индексов), это надо сделать ДО beginRemoveRows().

Но это вопрос решаемый, это надо просто иметь в виду.

Непонятно, что делать, если удаление может закончиться неудачно (например, по ряду таблиц я каскадное удаление не поддерживаю из соображений безопасности). Если после этого вызвать endRemoveRows() – неудалённая запись не будет отображаться. Если не вызвать – модель останется в непонятном состоянии. Я читал, в частности, что последующий вызов beginRemoveRows() может привести к непредсказуемым последствиям. Единственное, что приходит в голову – это что тут уж точно можно вызвать refresh() и не париться.

hobbit ★★★★★
() автор топика
Ответ на: комментарий от hobbit

удаление может закончиться неудачно

Вызовом beginRemoveRows модель декларирует, что удалит строку, потому должна её удалить. Если строка может не удалиться, то не вызывай beginRemoveRows, пока не удостоверишься, что условия удаления выполнены.

unC0Rr ★★★★★
()
Ответ на: комментарий от unC0Rr

В моём случае разделение на проверку и удаление это хорошее такое раздувание кода.

И вообще, один из смыслов разделения на beginRemoveRows() и endRemoveRows(), насколько я понимаю, был в том, что удаление может занять продолжительное время, и это всё надо как-то адекватно отображать. А если ещё и перед beginRemoveRows() будет что-то продолжительное – смысл слегка теряется.

Что ж, ладно.

hobbit ★★★★★
() автор топика
Последнее исправление: hobbit (всего исправлений: 2)
Ответ на: комментарий от hobbit

разделение на проверку и удаление это хорошее такое раздувание кода.

В смысле? Ну тогда делай beginRemoveRows ровно строкой кода ранее удаления, после чего сразу endRemoveRows.

удаление может занять продолжительное время

Не, смысла в продолжительном времени нет, т.к. виджет всё равно не перерисуется, пока твоя функция удаления не передаст управление в евентлуп. Это нужно, чтобы вьюхи могли обратиться к каким-нибудь пропертям удаляемых строк до того, как они потеряют к ним доступ.

unC0Rr ★★★★★
()
Ответ на: комментарий от unC0Rr

Ну и кроме раздувания всё-таки есть смутное подозрение, что это не всегда осуществимо.

Ладно, у меня банальная работа с БД, я метод удаления с проверкой могу разделить на проверку и удаление. А если, например, модель управляет чем-то удалённым (по сети), и у этого удалённого есть свои представления о прекрасном? (Да, это уже не тот случай, с которого я тему начал.)

hobbit ★★★★★
() автор топика
Ответ на: комментарий от unC0Rr

чтобы вьюхи могли обратиться к каким-нибудь пропертям удаляемых строк

А вот насчёт этого можно привести какие-то жизненные примеры? Штатные QTreeView и QTableView таким занимаются, например? Или это возможность, заложенная «на вырост»?

hobbit ★★★★★
() автор топика
Ответ на: комментарий от hobbit

модель управляет чем-то удалённым (по сети)

Ну а как работает QAbstractItemModel::data()? Прямо в сеть лезет и проверяет, на месте ли данные? Или есть локальная копия, ну или хотя бы какой-то идентификатор для доступа к данным? Когда удаляешь локальную возможность доступа к данным, тогда и вызывай beginRemoveRows/endRemoveRows.

А вот насчёт этого можно привести какие-то жизненные примеры? Штатные QTreeView и QTableView таким занимаются, например? Или это возможность, заложенная «на вырост»?

Не уверен на этот счёт. В момент beginRemoveRows удаляют активные редакторы для удаляемых строк, может быть полезно сделать это заранее. Но несложно представить и то, что вьюхе может быть нужно почистить какие-нибудь внутренние структуры в зависимости от данных в модели, например, банально декрементировать счётчик строк с отмеченными чекбоксами.

unC0Rr ★★★★★
()

хоть и нейронка но может поможет идея сгруппировать по родителю и отсортировать по индексам чтоб индексы друг другу не мешали

#include <QModelIndexList>
#include <QPersistentModelIndex>
#include <algorithm>

// Структура для удобной сортировки: сначала группируем по родителю, 
// затем сортируем строки внутри родителя по убыванию.
struct RemoveTask {
    QModelIndex parent;
    int row;
    
    // Сортировка: сначала родитель (по внутреннему указателю или ID), 
    // потом строка по убыванию!
    bool operator<(const RemoveTask& other) const {
        if (parent.internalPointer() != other.parent.internalPointer()) {
            return parent.internalPointer() < other.parent.internalPointer();
        }
        return row > other.row; // Убывание! Это критически важно.
    }
};

void MyTreeModel::removeIndexes(const QModelIndexList& indexesToRemove)
{
    if (indexesToRemove.isEmpty()) return;

    // 1. Собираем задачи на удаление. 
    // Лучше использовать QPersistentModelIndex, если есть риск, 
    // что модель изменится из другого места, но для простоты возьмем row и parent.
    std::vector<RemoveTask> tasks;
    tasks.reserve(indexesToRemove.size());

    for (const QModelIndex& idx : indexesToRemove) {
        if (!idx.isValid()) continue;
        tasks.push_back({idx.parent(), idx.row()});
    }

    // 2. Сортируем: группировка по родителю + убывание номеров строк
    std::sort(tasks.begin(), tasks.end());

    // 3. Выполняем удаление
    for (const auto& task : tasks) {
        // Сообщаем View, что удаляем одну строку (first == last)
        beginRemoveRows(task.parent, task.row, task.row);
        
        // --- ЗДЕСЬ ТВОЙ КОД УДАЛЕНИЯ ДАННЫХ ИЗ ВНУТРЕННЕЙ СТРУКТУРЫ ---
        // например: removeNodeFromMyTree(task.parent.internalPointer(), task.row);
        // ----------------------------------------------------------------
        
        endRemoveRows();
    }
}
cylon17
()
Ответ на: комментарий от unC0Rr

например, банально декрементировать счётчик строк с отмеченными чекбоксами.

А для этого вьюхе точно надо, чтобы внутренняя структура данных (за которую отвечает модель) ещё была жива? Другими словами, если я физические данные (даже в той же БД) удалю не сразу после, а непосредственно перед beginRemoveRows(), это помешает вьюхе декрементировать счётчик чекбоксов, да и сами чекбоксы грохнуть? Вроде бы лазить в БД вьюхе для этого не надо…

hobbit ★★★★★
() автор топика
Ответ на: комментарий от cylon17

Да, я для написания модели тоже DeepSeek мучал, но отсортировать индексы по убыванию перед удалением додумался и без него :)

hobbit ★★★★★
() автор топика
Ответ на: комментарий от hobbit

Зависит от имплементации data(). Если запрос состояния чекбокса не приводит к обращению к базе данных, то конечно можно данные в базе удалить перед вызовом beginRemoveRows.

unC0Rr ★★★★★
()
Ответ на: комментарий от Xintrea

Могу посоветовать посмотреть книги на русском по Qt.

Жасмин Бланшет, Марк Саммерфилд - Qt 4. Программирование GUI на C++Подобной информации в ней нет.

Боровский А. - Qt4.7+. Практическое программирование на C++ (2012)Подобной информации в ней нет.

Я даже всхрюкнул немного :)

Zhbert ★★★★★
()
Ответ на: комментарий от unC0Rr

Кстати, чисто для уточнения. Если для какого-нибудь tableview кажется, что количество отмеченных чекбоксов вьюха может подсчитать и так непосредственно из виджетов, которые она создаёт, напомню про существование прокси-моделей и что вьюха в принципе вообще не обязана создавать виджеты, простой счётчик количества отмеченных чекбоксов тоже может быть вьюхой к модели.

unC0Rr ★★★★★
()
  • Markdown
Пустая строка (два раза Enter) начинает новый абзац. Знак '>' в начале абзаца выделяет абзац курсивом цитирования.
Внимание: прочитайте описание разметки Markdown.
Используйте Ctrl-Enter для размещения комментария