QComboBox - Работа с моделями в Qt для отображения данных в виджетах. Часть 1
Сегодня мы рассмотрим создание простой модели для виджета QComboBox.
Обновлено 07.12.2020. В связи с выходом статьи, посвященной моделям, убрано вступление, оставлена только практическая часть!
Создание нового проекта
Создадим новый пустой проект Qt Widgets Application с главной формой, на форму добавим QComboBox.
Так же добавим на форму кнопку QPushButton и назовем её Add
Запустим проект:
Создание модели
Добавим новый класс в проект и назовем его 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 заполнится данными:
Добавим еще одну строчку в конец метода
values->append(QPair<int,QString>(46,"item1(6)"));
Запустим:
Как видите, простого добавления значения в список 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:
Чтобы решить эту проблему, добавим следующий код в конец нового метода:
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");
}
Таким образом мы в одну строчку можем обновлять элементы в 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, тут у нас ситуация такая же, как и с правкой строк:
Давайте сразу добавим в модель метод, для удаления строк из 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");
}
Обратите внимание, что при добавлении в позицию не изменяется текущий элемент:
Работа с несколькими строками в QComboBox
Учитывая специфику применения ComboBox, я не буду рассматривать вставку, изменения и удаление нескольких строк, а также перемещение строк в другую позицию.
Все вышеперечисленное будет рассмотрено в следующих статьях.
Заключение
Сегодня мы создали новый проект и рассмотрели добавление, правки и удаление строк в компоненте QComboBox.
Так же были рассмотрены особенности индексации и механизм оповещения модели об изменениях в данных.
В следующих статьях мы рассмотрим виджет QListView.
Скачать исходный код вы можете на GitFlic.
Добавить комментарий