Воскресенье, 22 ноября 2020 11:47

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

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

Сегодня мы рассмотрим создание простой модели для виджета QComboBox. О том что такое модели и зачем они нужны, вы можете прочитать в этой статье.

Обновлено 07.12.2020. В связи с выходом статьи, посвященной моделям, убрано вступление, оставлена только практическая часть!

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

Создадим новый пустой проект 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(newRow,QPair<int,QString>(data_idx, value));

    endInsertRows();
}

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

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

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

comboboxaddto.gif

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

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

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

Заключение

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

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

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

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

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