Понедельник, 07 декабря 2020 11:46

Модели, Представления и Индексы. Анатомия моделей в Qt5. Часть 1.

Россия
Оцените материал
(0 голосов)

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

  1. С использованием массива — в этом случае все элементы таблицы доступны по их индексам и вы можете легко получить или назначить значение ячейки таблицы, например: table[0][0]
  2. С помощью модели — в этом случае за наполнение таблицы данными отвечает модель — класс, который знает о таблице всё — количество строк и столбцов и значение каждой ячейки. Наполнение и доступ к информации, в данном случае, осуществляется с помощью строго определенных методов класса. Таким образом, внутренняя реализация модели не важна - мы всегда можем получить доступ к данным с помощью стандартизированного интерфейса.

В этой и последующих статьях я, иногда, буду называть виджеты Qt5 - компонентами.

Введение

Зачем нужна модель, если есть массив и его очень легко отобразить на экране? До тех пор, пока мы оперируем простым набором данных, вполне можно использовать обычный массив. Но, как только, возникает необходимость отобразить информацию из базы данных, сразу возникает целый ряд проблем:

  1. Дублирование данных — вам необходимо одновременно держать в оперативной памяти два набора данных. Один набор - базовый, полученный после выборки из базы данных, второй внутри виджета, в формате, подходящем для отображения виджетом.
  2. Синхронизация данных — как правило информация имеет тенденцию изменяться. Соответственно вам придется, каким-то образом, поддерживать массив с данными для таблицы в актуальном состоянии относительно БД, что выльется в расход памяти и процессорного времени.
  3. Трудность тестирования и отладки — учитывая вышесказанное написание тестов и отладка становятся самым настоящим испытанием и могут отнять у вас огромное количество времени.

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

Концепция Модель/Представление/Контроллер

Впервые концепция MVC была представлена в 1978 Трюгве Реенскаугом для языка SmallTalk. В последствии концепция эволюционировала и на данный момент поддерживается большим количеством языков программирования, например - PHP, Ruby, Python, а также C++ и другие.

Рассмотрим следующую схему:
MVC_Model.png
Всё начинается с запроса пользователя, который взаимодействует с Представлением, например, нажимает на кнопку, на схеме это «Пользовательский ввод».

Запрос поступает на Контролер, который, в идеале, выполняет только «маршрутизацию» запросов от пользователя и вызов отображения нужного Представления.

Контролер отправляет запрос к Модели, которая на основании внутренних алгоритмов и бизнес-логики, решает, например, сделать запрос к источнику данных и осуществляет SQL-запрос.

При этом модель не обязательно обязана сделать запрос к БД, это может быть и запрос к источнику данных, например - AJAX запрос или вообще запрос к датчикам Интернета Вещей (Internet of Things). Также это может быть набор алгоритмов бизнес-логики, которые основываясь на запросе от пользователя, делают запросы к БД или делают некие вычисления на основе алгоритма.

Далее, по цепочке, запрошенные данные от Модели попадают на Контролер. Контролер принимает решение в какое Представление передать данные и передает их.

Представление осуществляет отображение полученной информации основываясь на внутренних алгоритмах или шаблоне, тут всё зависит от Фреймворка.

Затем пользователь снова взаимодействует с Представлением, оно отправляет запрос к контроллеру с данными о действиях пользователя и всё начинается сначала. Этот процесс повторяется, пока работа программы не будет завершена.

Таким образом, основу MVC составляет отделение данных (модели) от её представления на экране, что, в свою очередь, позволяет решить следующие задачи:

  1. Уже созданную Модель можно использовать со множеством Представлений;
  2. Можно использовать Представление с разными Контролерами, таким образом не нужно каждый раз менять код самого Представления;
  3. Разработкой Модели или Представления могут заниматься разные разработчики, до тех пор, пока существует стандартизированный интерфейс.

Не существует строгой реализации MVC, разные Фреймворки по-разному реализуют данный функционал!

В идеале модель должна содержать в себе вообще всю бизнес-логику, а Контролер «маршрутизировать» запросы между Представлением, Моделью и пользователем. Очень часто поступают по-другому и весь функционал помещают в контролер, что в корне неверно!

Концепция Модель/Представление

Qt5, для работы с виджетами, использует архитектуру Модель/Представление (Model/View). В отличие от традиционного подхода в Qt Представление и Контролер объединены, таким образом данные все еще отделены от их представления на экране, но также это позволяет упросить Фреймворк, нам не нужно создавать отдельный класс для Контролера. Так же подобное разделение только на две сущности позволяет нам создавать новые типы представления, но использовать в них одну и ту же модель, не меняя каждый раз структуру данных.

Рассмотрим рисунок:

MV_Model.png

При данной архитектуре всё происходит также, как и в MVC, но всю работу по маршрутизации запросов между пользователем и Моделью берет на себя Представление.

На данной схеме отсутствует Делегат, но о нем мы поговорим в следующей статье и тогда же рассмотрим полную схему.

Плюсы использования концепции Модель/Представление:

  1. Нам больше не нужно беспокоится о том, что объем данных слишком велик, чтобы уместиться в оперативной памяти устройства и извращаться, чтобы отобразить данные. Все что от нас требуется — создать класс с Моделью, которая будет считывать данные из источника данных и передать её виджету, который сам позаботиться об их отображении.
  2. Благодаря унифицированному интерфейсу, Модель может быть использована множеством виджетов.
  3. Представление использует Модель для доступа к данным, а значит упрощается отладка и тестирование кода.

Как работает модель в Qt5

Давайте рассмотрим, как работает Модель в Qt5. Для этого мы будем использовать проект из статьи.

Наша Модель наследуется от абстрактного класса QAbstractListModel, который можно использовать с простыми списками.

Для создания простой модели достаточно реализовать два метода:

  • int rowCount(const QModelIndex &) const;
  • QVariant data(const QModelIndex &index, int role) const;

Первый метод возвращает количество строк, второй возвращает непосредственно данные для строки заданной Индексом index имеющей роль (role).

Рассмотрим Индексы и Роли более подробно.

Индексы модели

Всё что требуется от Модели – знать каким образом можно извлечь из источника данных необходимую информацию, её тип, структура и сами значения не важны. Для обеспечения точной и однозначной адресации был введен класс QModelIndex.

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

В случае, когда данные представлены простым списком или таблицей, для каждого элемента с помощью метода QAbstractItemModel::createIndex() создается свой Индекс, а для поля parent указывается Недействительный индекс.

Недействительные индексы (invalid index)

Недействительный (invalid index) - Индекс, который может быть создан с помощью вызова конструктора QModelIndex без параметров:

QModelIndex() 

Недействительный индекс используется как заглушка, когда значение поля parent не задано. Так же такой Индекс может быть возвращен методами классов, которые работают с Моделями, при возникновении различных ошибок.

Как проверить является ли индекс недействительным

Некоторые методы в Qt5 возвращают Недействительный индекс в случае ошибки. Чтобы проверить является ли он недействительным можно использовать метод isValid()

Например, данный код:

QModelIndex invalid = QModelIndex();
QModelIndex valid = model->index(0, 0, QModelIndex());

qDebug() << "Valid index = " << invalid.isValid();
qDebug() << "Valid index = " << valid.isValid();

Выведет:

Valid index = false
Valid index = true

Использование индексов

Так как данные внешнего источника могут в любой момент измениться, нельзя сохранять и использовать сохраненные значения номеров строк, столбцов и прочие данные Индекса в бизнес-логике модели! При удалении или перемещении строк данные Индексы могут стать неверными, что приведет к непредсказуемым результатам, это важно помнить!

В документации Qt5 особенно подчеркивается

Note: Model indexes should be used immediately and then discarded. You should not rely on indexes to remain valid after calling model functions that change the structure of the model or delete items.

Обратите внимание: Индексы модели должны использоваться сразу после создания и затем удаляться. Вы не должны полагаться на то, что индекс останется актуальным после вызова методом, которые изменяют или удаляют данные.

Таким образом создавая Индексы используйте локальные переменные и не храните их глобально!

Индекс не хранит никаких указателей на данные, только указатель на саму Модель, таким образом, если вы используете несколько Моделей, вы с легкостью можете получить доступ к модели, что помогает сделать код более предсказуемым и предотвращает путаницу.

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

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

Создать индекс можно с помощью метода index() самой модели, например код:

QModelIndex idx = model->index(0, 0, QModelIndex());

qDebug() << "Display value: " << model->data(idx,Qt::DisplayRole);
qDebug() << "Data value: " << model->data(idx,Qt::UserRole);

Выведет:

Display value: QVariant(QString, "Select item")
Data value: QVariant(int, -1)

Строки и столбцы

Табличная модель

Во всех виджетах, работающих с простыми списками и таблицами, используется Табличная модель:

modelview-tablemodel.png 


В данном типе модели все элементы определяются номером строки и столбца.

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

Чтобы получить индекс для заданной ячейки таблицы используется код:

QModelIndex idxA = model->index(0, 0, QModelIndex());
QModelIndex idxB = model->index(1, 1, QModelIndex());
QModelIndex idxC = model->index(2, 1, QModelIndex());

QComboBox тоже использует эту модель, просто значение столбца всегда будет равно 0! 

Третьим параметром нужно указывать недействительный индекс QModelIndex()

Данный параметр бесполезен в Табличной модели и используется в Древовидной модели.

Древовидная модель

Кроме табличного представления данных, очень часто используется представление в виде дерева. Простым примером может служить древовидный список QTreeView:

modelview-treemodel.png

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

QModelIndex idxA = model->index(0, 0, QModelIndex());

QModelIndex idxB = model->index(1, 0, idxA);

Таким образом вы можете получить доступ к древовидной структуре. Более подробно этот механизм мы рассмотрим, когда будем говорить о QTreeView.

Роли индексов

Как уже было сказано, Индекс не хранит никаких указателей на данные, только указатель на саму Модель, а для корректного отображения элементов на экране нам может потребоваться несколько значений, например, для QComboBox это значение строки, отображаемый текст строки, возможно, иконка для строки, её стили – цвет фона и так далее. Чтобы не создавать новые структуры для хранения этих данных была введена концепция ролей.

Для каждого элемента можно задать несколько Ролей. Давайте рассмотрим рисунок:

 modelview-roles.png

Как видно, для каждого элемента, в данном случае списка, используются несколько Ролей.

  • DisplayRole – отображаемое значение;
  • DecorationRole – в данном случае иконка для списка;
  • ToolTipRole – всплывающая подсказка;
  • UserRole - если мы хотим, сопоставить каждой строке некое значение, мы можем использовать эту Роль.

Таким образом, если нам нужно получить значение выбранного элемента, в QComboBox мы можем использовать код:

QString data = index.model()->data(index, Qt::UserRole).toString();

Роли будут рассмотрены будущих статьях, в которых будет описана работа с делегатами.

Определение ролей

На примере модели QComboBox, рассмотрим, как назначается Роль для элемента.

Для каждой модели мы должны реализовать метод data:

QVariant QComboBoxModel::data( const QModelIndex &index, int role ) const
{
    QVariant value;

        switch ( role )
        {
            case Qt::DisplayRole: //string
            {
                value = this->values->value(index.row()).second;
            }
            break;

            case Qt::UserRole: //data
            {
            value = this->values->value(index.row()).first;
            }
            break;

            default:
                break;
        }

    return value;
}

В качестве параметра функции data передается Индекс и Роль.

Мы проверяем, совпадает ли переданная Роль с одной из поддерживаемой моделью, если нет – то возвращаем пустое значение.

Если Роль поддерживается, мы используем Индекс, чтобы получить номер строки и возвращаем значение из источника данных, соответствующее Роли.

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

case Qt::DecorationRole: //image
{
value = this->values->value(index.row()).second.getIcon();
}

Ролей может быть сколько угодно, вы даже можете определить свои Роли, но об этом мы поговорим в будущих статьях.

Заключение

Сегодня мы рассмотрели Модели в QT5, причины их появления и плюсы использования.

Мы рассмотрели две концепции Модель/Представление/Контроллер, и использующуюся в QT5 Модель/Представление.

Разобрали как работает Модель в Qt5, что такое Индексы и Роли, для чего нужны Недействительные индексы.

Была рассмотрена табличная и древовидная модель представления строк и столбцов, а также способы определения Ролей.

Прочитано 1897 раз Последнее изменение Среда, 13 января 2021 12:14