QComboBox - Работа с моделями в Qt5 для отображения данных в виджетах - АлтунинВВ.Блог - всё об IT-технологиях!
Воскресенье, 22 ноября 2020 11:47

QComboBox - Работа с моделями в Qt5 для отображения данных в виджетах

Россия

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

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

2. С помощью модели — в этом случае за наполнение таблицы данными отвечает модель — класс, который знает все о таблице — количество строк и столбцов и значение каждой ячейки. Наполнение и доступ к информации в данном случае осуществляется за счет методов класса. Таким образом нам по большому счету не важно, как внутри реализована модель, так как для доступа у нас есть публичные методы класса.

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

Введение

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

  1. Дублирование данных — вам необходимо одновременно держать в оперативной памяти два набора данных. Представим, что нам необходимо отобразить массив данных в 100000 строк. Сколько памяти займет такой массив данных? Если мы возьмем простой телефонный справочник с ФИО, адресом и телефоном, то это уже приблизительно 50-70 мегабайт на 100000 строк.  Вроде немного, но учтите, что нам придется хранить его 2 раза, один раз считывая из БД, а второй отрисовывая таблицу, а это уже около 100-150 мегабайт.
  2. Синхронизация данных — как правило информация имеет тенденцию изменяться. Соответственно вам придется, каким-то образом, поддерживать массив с данными для таблицы в актуальном состоянии относительно БД, что выльется в расход памяти и процессорного времени.
  3. Трудность тестирования и отладки — учитывая вышесказанное написание тестов и отладка становятся самым настоящим испытанием и могут отнять у вас огромное количество времени.

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

Модель/Представление в Qt5

В Qt5 используется технология Модель/Представление. Эта технология позволяет отделить данные от их представления на экране, и может сильно нам облегчить жизнь:

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

Класс QAbstractItemModel

Чтобы облегчить программисту жизнь в Qt5 уже существует абстрактный класс — QAbstractItemModel. Любой класс, который реализует методы класса QAbstractItemModel считается виджетами моделью. Так что вам достаточно передать ссылку на вашу модель в виджет и он будет отображать данные с помощью модели, а так же, сможет их модифицировать, например редактировать, если это предусмотрено самой моделью.

Виды виджетов использующих Модель/Представление

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

  • QComboBox
  • QListView
  • QTreeView
  • QTableView
  • QColumnView
  • QUndoView

В этой статье мы рассмотрим QComboBox –выпадающий список.

Создание нового проекта

Создадим новый пустой проект Qt Widgets Application с главной формой, на форму добавим QComboBox.

Так же добавим на форму кнопку QPushButton и назовем её Add

Запустим проект:

2020-11-18_12-19-08.png

Создание модели

Добавим новый класс в проект и назовем его QComboBoxModel это будет наша модель.

Добавим код

Заголовки:

#ifndef QCOMBOBOXMODEL_H
#define QCOMBOBOXMODEL_H

#include <QModelIndex>

class QComboBoxModel : public QAbstractListModel
{
public:
    QComboBoxModel(QObject *parent=nullptr);
    int rowCount(const QModelIndex &) const;
    QVariant data(const QModelIndex &index, int role) const;
private:
    QList<QPair<int,QString>> *values;
};

#endif // QCOMBOBOXMODEL_H

Обратите внимание на строчку:

QList<QPair<int,QString>> *values;

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

Исходный код

#include "qcomboboxmodel.h"

#include <QModelIndex>
#include <QDebug>

QComboBoxModel::QComboBoxModel(QObject *parent)
    :QAbstractListModel(parent)
{
    values = new QList<QPair<int,QString>>();
}

int QComboBoxModel::rowCount(const QModelIndex &) const
{
    return values->count();
}

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

Запустим проект, чтобы убедиться в отсутствии ошибок.

Теперь нам нужно заполнить нашу модель данными:

В метод MainWindow::MainWindow добавим после ui->setupUi(this); следующий код:

values = new QList<QPair<int,QString>>();

values->append(QPair<int,QString>(-1,"Select item"));
values->append(QPair<int,QString>(10,"item1(0)"));
values->append(QPair<int,QString>(11,"item1(1)"));
values->append(QPair<int,QString>(21,"item1(2)"));
values->append(QPair<int,QString>(32,"item1(3)"));
values->append(QPair<int,QString>(44,"item1(4)"));

Добавим в mainwindow.h в раздел private строки

QList<QPair<int,QString>> *values;
QComboBoxModel *model;

Добавим в модель метод для заполнения нашего ComboBox данными.

void QComboBoxModel::populate(QList<QPair<int,QString>> *newValues)
{
    int idx = this->values->count();
    this->beginInsertRows(QModelIndex(), 1, idx);
        this->values = newValues;
    endInsertRows();
 }

 Добавим в конец MainWindow::MainWindow код 

   model = new QComboBoxModel();

    model->populate(values);

    this->ui->comboBox->setModel(model);

Запустим проект и у нас ComboBox заполнится данными:

2020-11-18_13-02-46.png

Добавим еще одну строчку в конец метода

values->append(QPair<int,QString>(46,"item1(6)"));

 

Запустим:

2020-11-18_13-56-44.png

Как видите, простого добавления значения в список values достаточно, чтобы обновить значения ComboBox.

 Добавим слот для нашей единственной кнопки QPushButton

void MainWindow::on_pushButton_clicked()

{

    values->append(QPair<int,QString>(99,"New Item"));

}

 

Запустим, откроем список и нажмем на кнопку и ничего не происходит!

Почему? Это происходит из-за того, что модель не была оповещена о том, что в данных произошли изменения.

Обратите внимание на метод populate:

    int idx = this->values->count();

    this->beginInsertRows(QModelIndex(), 1, idx);

        this->values = newValues;

    endInsertRows();

Для того, чтобы уведомить модель о том, что у нас появились новые строки данных, мы используем методы beginInsertRows и endInsertRows.

Метод beginInsertRows вызывается перед добавлением новых строк данных, а метод endInsertRows после окончания добавления этих данных.

beginInsertRows принимает три параметра, в первый мы просто передаем QModelIndex(), второй и третий параметры соответственно индексы первого элемента и индекс последнего элемента.

Так как метод populate предназначен для полного обновления списка, мы вызываем метод с параметром 1 и максимальным размером списка.

Обратите внимание, использование beginInsertRows и endInsertRows обязательно!

Рассмотрим наш случай. Если закомментировать эти два метода, то сразу после запуска если вы будете нажимать кнопку, то в ComboBox будет добавлено нужное количество элементов, а если вы сначала откроете список, чтобы проверить количество элементов и нажмете кнопку, то новые строки будут добавлены в список values, но ComboxBox не будет обновлён!

Добавление строки в QComboBox

Но обновлять весь список, когда мы добавляем только один элемент неправильно, давайте добавим отдельный метод для добавления только одной строки в наш ComboBox.

void QComboBoxModel::append(int index, QString value)

{

    int newRow = this->values->count()+1;

 

    this->beginInsertRows(QModelIndex(), newRow, newRow);

        values->append(QPair<int,QString>(index,value));

    endInsertRows();

}

В данном методе мы вычисляем индекс добавляемого элемента и затем, используя beginInsertRows и endInsertRows, добавляем данные в наш QList, таким образом уведомляя модель, о том, что у нас изменились данные.

Изменим слот для кнопки Add

void MainWindow::on_pushButton_clicked()
{
        model->append(99,"New Item");
}

 

Теперь строки добавляются корректно.

Изменение строки в QComboBox

Давайте теперь попробуем внести изменения в строки нашего QComboBox.

Добавим новую кнопку и назовем ее Edit

Добавим для нее слот:

void MainWindow::on_pushButton_2_clicked()
{
    (*this->values)[ui->comboBox->currentIndex()].second = "New row value";
}

Запустим, при нажатии на кнопку ничего не происходит. На самом деле происходит изменение названия текущей строки, но она обновится после того, как вы наведете курсор мыши на QComboBox:

comboboxedit.gif

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

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

    emit ui->comboBox->model()->dataChanged(idx,idx);

Запустим наш проект. Теперь строка в QComboBox обновляется сразу после нажатия на кнопку Edit.

У нас получился метод:

void MainWindow::on_pushButton_2_clicked()

{

    (*this->values)[ui->comboBox->currentIndex()].second = "New row value";

 

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

    emit ui->comboBox->model()->dataChanged(idx,idx);

}

Давайте рассмотрим его подробнее:

Первая строка

(*this->values)[ui->comboBox->currentIndex()].second = "New row value";

Просто изменяет строковое значение текущего элемента QComboBox.

Обратите внимание на запись:

(*this->values)[]

Вам необходимо использовать подобное разыменование указателя C++ и квадратные скобки.

У класса QList есть метод at(), позволяющий получить элемент списка по его индексу, но в нашем случае, нам он не подходит. Почему? Давайте рассмотрим его определение:

const T &QList::at(int i) const

Обратите на const, это слово означает, вкратце, что данные, которые возвращает объект, нельзя изменить никаким образом, компилятор просто выдаст ошибку. Таким образом нам остается только оператор [], который избавлен от этого ограничения:

T &QList::operator[](int i)

Таким образом единственный способ изменить элемент списка QList – это использовать разыменование (*this->values)[]

Давайте так же рассмотрим индексацию элементов QComboBox.

Индексация элементов в QComboBox

Все элементы в QComboBox индексируются с 0. Следует различать индексы строк QComboBox и индексы значений строк.

Например, при запуске следующий код

qDebug() << (*this->values)[0];

Выведет следующее

QPair(-1,"Select item")

Т.е. значения полей класса QPair.

Если же мы добавим строку:

qDebug() << (*this->values)[0].first;

будет выведено

-1

Т.е. именно значение, которое мы поставили в соответствие к строке Select item.

Когда модель оперирует индексами строк и столбцов она оперирует внутренними индексами, которые не имеют никакого отношения к тем значениям, что мы задаем, это важно помнить!

Вернемся к разбору метода on_pushButton_2_clicked()

Рассмотрим строки

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

    emit ui->comboBox->model()->dataChanged(idx,idx);

Первая получает объект класса QModelIndex для текущей строки.

Метод currentIndex принимает два параметра – первый строка, второй – столбец.

Так как у нас в QComboBox всего один столбец, второй параметр мы всегда устанавливаем в 0.

Метод ui->comboBox->currentIndex() возвращает индекс текущего элемента QComboBox, так что мы передаем его в первом параметре.

Самая важная строчка здесь это

emit ui->comboBox->model()->dataChanged(idx,idx);

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

Данный сигнал принимает два параметра – оба должны являться экземплярами класса QModelIndex, первый параметр - это строка, а второй это столбец.

Здесь мы указываем два раза полученный нами ранее индекс idx, так как обновляется только один элемент.

Таким же образом мы можем обновить и любой элемент с любым индексом, для примера обновим элемент ComboBox с индексом 4:

(*this->values)[4].second = "New row value 4";

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

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

void QComboBoxModel::update(int idx, QString value)
{
    (*this->values)[idx].second = value;
 
   QModelIndex item_idx = this->index(idx,0);
    emit this->dataChanged(item_idx ,item_idx );
}

 И изменим слот для кнопки Edit

void MainWindow::on_pushButton_2_clicked()
{
    model->update(ui->comboBox->currentIndex(),"New row value");
    model->update(4,"New row value 4");
}

comboboxedit1.gif 

Таким образом мы в одну строчку можем обновлять элементы в ComboBox.

Подобным образом мы можем обновить и индексы данных, связанные со строками нашего ComboBox, код:

qDebug() << (*this->values)[0].first;

(*this->values)[0].first = -20;

qDebug() << (*this->values)[0].first;

Выведет:

-1
-20

Удаление строк из QComboBox

Добавим новую кнопку на форму и назовем её Del

Добавим для этой кнопки слот:

void MainWindow::on_pushButton_3_clicked()
{
    (*this->values).removeAt(0);
}

Запустим и нажмем на Del, тут у нас ситуация такая же, как и с правкой строк:

comboboxdel.gif

Давайте сразу добавим в модель метод, для удаления строк из ComboBox:

void QComboBoxModel::deleteRow(int idx)
{
    int rowIdx = this->values->count()+1;

    this->beginRemoveRows(QModelIndex(), idx,idx);

        (*this->values).removeAt(idx);

    this->endRemoveRows();
}

Обратите внимание, мы используем методы beginRemoveRows и endRemoveRows, чтобы корректно уведомить модель об удалении строк!

Изменим слот для кнопки Del

void MainWindow::on_pushButton_3_clicked()
{
    model->deleteRow(0);
}

Если вы попробуете удалить все строки, то после удаления последней, в консоли получите ошибку:

QList::removeAt(): Index out of range.

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

Вставка строк в нужную позицию в QComboBox

Возможно, вам понадобится вставить в QComboBox строку в определённую позицию.

Добавим на форму кнопку AddTo и создадим для нее слот: 

void MainWindow::on_pushButton_4_clicked()
{
    values->insert(0,QPair<int,QString>(-2,"Pre Select item"));

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

    emit model->dataChanged(item_idx, item_idx );
}

Для вставки строки в заданную позицию мы используем метод insert, первым параметром мы передаем индекс, перед которым мы хотим вставить элемент, вторым параметром данные вставляемой строки.

Давайте добавим в нашу модель метод для реализации этого функционала:

void QComboBoxModel::insertAt(int idx, int data_idx, QString value)
{
    int newRow = idx;

    this->beginInsertRows(QModelIndex(), newRow, newRow);

        values->insert(0,QPair<int,QString>(data_idx, value));

    endInsertRows();
}

Изменим слот для кнопки

void MainWindow::on_pushButton_4_clicked()
{
    model->insertAt(0, -2,"Pre Select item");
}

Обратите внимание, что при добавлении в позицию не изменяется текущий элемент:

comboboxaddto.gif

Работа с несколькими строками в QComboBox

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

Все вышеперечисленное будет рассмотрено в следующих статьях.

Заключение

Сегодня мы рассмотрели модели – что это такое и для чего они нужны.

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

Были рассмотрены модели в QT5 и основные компоненты, которые поддерживают модели.

Создали новый проект и рассмотрели добавление, правки и удаление строк в компоненте QComboBox.

Так же были рассмотрены особенности индексации и механизм оповещения модели об изменениях в данных.

В следующих статьях мы рассмотрим виджет  QListView.

Скачать исходный код вы можете на Github.

Прочитано 179 раз Последнее изменение Воскресенье, 22 ноября 2020 13:53