Pull to refresh

Создание моделей данных для QComboBox

Reading time10 min
Views25K


Всем привет! Хочу поделиться с вами двумя способами, как можно и нужно создавать модели данных для виджетов типа QComboBox в Qt. В конце статьи будет показано решение, для заполнения комбобокса из БД, одной строкой кода.

Способ №1. Полностью ручное создание модели


Все модели данных в Qt должны быть наследниками от QAbstractItemModel. Лично в моей практике комбобоксы всегда отображали перечисление из SQL базы данных. Это были пол, страна, национальность и некоторые другие списки, из которых пользователю нужно было выбрать один пункт. Поэтому, при создании модели, у меня всегда было две параллельных задачи:

  1. Как сформировать человекочитаемые названия пунктов пользователю?
  2. Как связать читаемые пункты с ключами, которые надо писать в БД?

На всякий случай поясню разницу, если кому не понятно. Первый пункт это человекочитаемое название пункта. В моём примере комбобокс для выбора национальностей. Там будут слова типа «Russian», «Belgian», «Norwegian» и т.п. То что пользователь программы увидит на экране. Второе это то что программа будет записывать в базу данных. Условно «служебное значение». В моём примере в базу пишется строка типа: «russian», «belgian», «norwegian». Это позволяет менять видимые пользователю названия пунктов без лишних хлопот. Например, дали вам задание уменьшить ширину комбобокса, за счёт сокращения названий национальностей. Вам надо показывать не «Russian», а «Rus.». В этом случае вы спокойно меняете текст выводимый для пользователя и закрываете задачу. Если же в базу писать прямо то что видно в комбобоксе. Изменние «Russian» -> «Rus.» заставит писать процедуры для базы данных. С целью перевода старых имен в новые. Что бы не потерялись уже выбранные национальности в базах конечных пользователей. Короче два описанных названия(человекочитаемое, служебное) для каждого пункта. Это хорошая практика создания поддерживаемого кода.


Для реализации замысла. Первым делом надо посмотреть какие из методов QAbstractItemModel, вы обязаны определить у себя:

  • QModelIndex QAbstractItemModel::index(int row, int column, const QModelIndex & parent = QModelIndex()) const
  • QModelIndex QAbstractItemModel::parent(const QModelIndex & index) const
  • int QAbstractItemModel::columnCount(const QModelIndex & parent = QModelIndex()) const
  • int QAbstractItemModel::rowCount(const QModelIndex & parent = QModelIndex()) const
  • QVariant QAbstractItemModel::data(const QModelIndex & index, int role = Qt::DisplayRole) const

Т.е. здесь перечислены «полностью виртуальные»(«pure virtual») методы. Казалось бы самым странным придётся реализовывать columnCount(). Т.к. очевидно что колонка одна. Потом index() и parent() выглядят как то избыточно, на фоне простой линейной структуры данных(список). Они нужны больше для построения иерархических моделей типа деревьев для QTreeView. Поэтому, что бы не выдумывать себе лишнюю работу, было решено наследовать класс модели от QAbstractListModel, который тоже годен в нашем случае. И требует реализовать только два последних(«pure virtual») метода из списка.


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

// nationalitymodel.h
// #pragma once
#include <QAbstractListModel>

class NationalityModel : public QAbstractListModel
{
	Q_OBJECT

	typedef QPair<QVariant, QVariant> DataPair;
	QList< DataPair > m_content;

public:
	explicit NationalityModel( QObject *parent = 0 );
	virtual QVariant data( const QModelIndex & index, int role = Qt::DisplayRole ) const;
	virtual int rowCount( const QModelIndex & parent = QModelIndex() ) const;
};

// nationalitymodel.cpp
#include "nationalitymodel.h"

NationalityModel::NationalityModel(QObject *parent) :
	QAbstractListModel(parent)
{
	m_content << qMakePair( DataPair::first_type(), DataPair::second_type( "" ) )
			<< qMakePair( DataPair::first_type( "Russian" ), DataPair::second_type( "russian" ) )
			<< qMakePair( DataPair::first_type( "Belgian" ), DataPair::second_type( "belgian" ) )
			<< qMakePair( DataPair::first_type( "Norwegian" ), DataPair::second_type( "norwegian" ) )
			<< qMakePair( DataPair::first_type( "American" ), DataPair::second_type( "american" ) )
			<< qMakePair( DataPair::first_type( "German" ), DataPair::second_type( "german" ) );
}

QVariant NationalityModel::data( const QModelIndex &index, int role ) const
{
	const DataPair& data = m_content.at( index.row() );
	QVariant value;

	switch ( role )
	{
		case Qt::DisplayRole:
		{
			value = data.first;
		}
		break;

		case Qt::UserRole:
		{
			value = data.second;
		}
		break;

		default:
			break;
	}

	return value;
}

int NationalityModel::rowCount(const QModelIndex &/*parent*/) const
{
	return m_content.count();
}

// addressbookmainwindow.cpp. В конструкторе формы, где будет использоваться модель ( AddressBookMainWindow::AddressBookMainWindow() )
ui->nationalityCombo->setModel( new NationalityModel( this ) );


Все значения пунктов комбобокса, просто записываються в QList< DataPair &gt m_content;. И потом выдаются при обращении комбобокса к функции NationalityModel::data(). Начинающим важно понимать. Не программист явно вызывает эту функцию в своём коде. А комбобокс обращается к данной функции когда ему надо! Ваша задача, что бы функция отдавала эти актуальные данные по запросу.


NationalityModel::data() вызывается с двумя параметрами. Как того требует простотип QAbstractItemModel::data():

  • const QModelIndex &index. Объект содержащий номер строки, колонки и ссылку на родительский QModelIndex. Т.е. QComboBox сообщает место(позицию) пункта, для которого запрашиваются данные. В нашем случае актуален только номер строки. Остальные параметры внутри &index только для совместимости с другими моделями, типа QTreeView и QTableView. Поэтому наша функция запрашивает пару, «читаемое» и «служебное» значения (DataPair) только для данной строки. Хранящиеся в списке возможных значений (m_content).
  • int role. В этом параметре QComboBox сообщает, какого рода данные нужны(какая роль). В нашем случае «читаемое» значение это Qt::DisplayRole, а «служебное» Qt::UserRole.

За один вызов NationalityModel::data() возвращаются данные одной роли для одной, конкретной строки в списке.


Если обратится к enum ItemDataRole, где определены Qt::DisplayRole, Qt::UserRole. Станет понятно для чего ещё можно реализовать такую модель. Например, поменять шрифт некоторых пунктов (Qt::FontRole). Выровнять текст пункта меню, как то по особенному. Или задать текст всплывающей подсказки. Смотрите в упомянутый enum. Возможно вы найдёте там то что искали уже давно.


Исходный код примера


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


Как скачать код с github.com
Начальная настройка проекта:
  1. Скачайте проект «git clone https://github.com/stanislav888/AddressBook.git»
  2. Меняете текущий каталог «cd AddressBook»
  3. Инициализируйте подмодуль «git submodule init»
  4. Подгружаете код подмодуля в проект «git submodule update»
  5. Открываете и собираете проект
  6. Запускаете программу
  7. Если всё хорошо, появиться окно выбора\создания файла базы данных. Можете посмотреть что за программа. Для заполнения тестовыми данными есть кнопочка «Fill test data»

Для сборки надо иметь QtCreator c Qt не ниже 5.0. Лично я собирал проект с Qt 5.5.0 компилятором gcc 5.3.1. Хотя проект будет собираться и даже работать на Qt 4.8.1. Для отладки БД можно использовать расширение для Firefox SQLite Manager.


Способ №2. Быстрое создание модели из перечисления в SQL БД


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


Для реализации нам потребуется QSqlQueryModel. Это похожая модель. Она тоже наследник QAbstractItemModel, но используется для отображения результатов SQL запроса QSqlQuery в таблице QTableView. В данном случае, наша задача приспособить данный класс. Что бы он давал данные так же как в первом примере.


Вы удивитесь, но код получился небольшим.

// addressdialog.h
/// #pragma once
#include <QSqlQueryModel>

class BaseComboModel : public QSqlQueryModel
{
	Q_OBJECT

	QVariant dataFromParent(QModelIndex index, int column) const;

public:
	explicit BaseComboModel( const QString &columns, const QString &queryTail, QObject *parent = 0 );
	virtual QVariant data(const QModelIndex &item, int role = Qt::DisplayRole) const;
	virtual int rowCount(const QModelIndex &parent) const;
};

// basecombomodel.cpp
#include "basecombomodel.h"
#include <QSqlQuery>

namespace
{
	enum Columns // Depends with 'query.prepare( QString( "SELECT ... '
	{
		Id,
		Data,
	};
}

BaseComboModel::BaseComboModel( const QString& visualColumn, const QString& queryTail, QObject *parent ) :
	QSqlQueryModel( parent )
{
	QSqlQuery query;
	query.prepare( QString( "SELECT %1.id, %2 FROM %3" ).arg( queryTail.split( ' ' ).first() ).arg( visualColumn ).arg( queryTail ) );
	// I.e. query.prepare( "SELECT country.id, countryname || ' - ' || countrycode  FROM country" );
	query.exec();
	QSqlQueryModel::setQuery( query );
}

QVariant BaseComboModel::dataFromParent( QModelIndex index, int column ) const
{
	return QSqlQueryModel::data( QSqlQueryModel::index( index.row() - 1 // "- 1" because make first row empty
														, column ) );
}

int BaseComboModel::rowCount(const QModelIndex &parent) const
{
	return QSqlQueryModel::rowCount( parent ) + 1; // Add info about first empty row
}

QVariant BaseComboModel::data(const QModelIndex & item, int role /* = Qt::DisplayRole */) const
{
	QVariant result;

	if( item.row() == 0 ) // Make first row empty
	{
		switch( role )
		{
			case Qt::UserRole:
				result = 0;
				break;
			case Qt::DisplayRole:
				result = "(please select)";
				break;
			default:
				break;
		}
	}
	else
	{
		switch( role )
		{
			case Qt::UserRole:
				result = dataFromParent( item, Id );
				break;
			case Qt::DisplayRole:
				result = dataFromParent( item, Data );
				break;
			default:
				break;
		}
	}

	return result;
}

// Использование модели в форме(addressdialog.ui) выглядит примерно так
ui->countryCombo->setModel(  new BaseComboModel( "countryname || ' - ' || countrycode", "country",  this ) );




В данной реализации, всю работу делает QSqlQueryModel. Надо только немного переопределить логику QSqlQueryModel::data(). Для начала представьте, что в модель записывается SQL запрос «SELECT country.id, countryname || ' — ' || countrycode FROM country».

Конечно в коде проекта это немного более замысловато. Но если отладить там будет сформирована именно такая строка. Запрос выводит два столбца. Первичный ключ(«id»). И человекочитаемое значние, видимое на скриншоте. Поскольку все результаты SQL запроса оказываются в Qt::DisplayRole у QSqlQueryModel. То без изменения QSqlQueryModel, в качестве модели комбобокса, выдаст просто список «id». А человекочитаемое значение не будет видно. Т.к. комбобокс никак не использует второй столбец модели(запроса). Вы это увидите, если закомментируете объявление и реализацию BaseComboModel::data().

Для того что бы увидеть список стран, как на скриншоте, BaseComboModel::data():

  • возвращает данные первого запрошенного столбца(«id») как Qt::UserRole первого столбца
  • возвращает данные второго столбца(«countryname || ' — ' || countrycode») как Qt::DisplayRole первого столбца
  • добавляет строку "(please select)" в самом начале. За счёт смещения номеров при запросе данных от QSqlQueryModel. Т.е. к результатам SQL запроса, модель сама добавляет ещё одну строку

Таким образом вы можете быстро и легко делать модели для QComboBox-а с помощью BaseComboModel. Например, у вас есть SQL таблица месяцев в году(«months»). Где два столбца, «id» и «monthname». Вам можно заполнить комбобокс выбора месяца следующим образом:

ui->monthsCombo->setModel( new BaseComboModel( «monthname», «months», this ) );
Получить значение «id» выбранного месяца ui->monthsCombo->itemData( ui->monthsCombo->currentIndex(), Qt::UserRole );. Получть значение видимое пользователю ui->monthsCombo->currentText();. Этот код гораздо компактнее всех остальных случаев. Большинство разработчиков в данной ситуации пишут, отдельно запрос к базе (QSqlQuery). А потом, в цикле, добавляют полученные записи в комбобокс, через QComboBox::addItem(). Это конечно рабочее, но не самое красивое решение.


Практика


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


Два варианта как это сделать:

  1. Эксперименты на основе моего приложения — адресной книги, упомянутой выше. Заголовок и реализация BaseComboModel уже присутствуют в проекте. Примеры ниже, будут на её основе.

  2. Использовать любое другое приложение работающее с SQL БД. Это не обязательно должна быть SQLite. Подойдёт любая база! Вы можете просто вставить код из листинга выше, в файл реализации любой формы.
    Конечно, было бы правильно сделать отдельные файлы, заголовочный и реализацию для BaseComboModel. Будем считать что пока нам это делать лень. Вам конечно придётся немного побороться с ошибками компиляции. Но они будут простыми. В таблицах, из которых вы будете брать данные для комбобокса. Обязан присутствовать столбец «id»

Параметры конструктора BaseComboModel( const QString &columns, const QString &queryTail, QObject *parent = 0 ):

  • const QString &columns. Формирование человекочитаемого названия пункта для пользователя. В примере выше «countryname || ' — ' || countrycode» применяется конкатенация двух столбцов через дефис. Оператор конкатенации "||" специфичный для SQLite. Можно указать несколько столбцов через запятую. Но показываться будет только первый.

  • const QString &queryTail. «Хвост» запроса. Содержимое SQL запроса после «FROM». Очевидно, в этой строке, сначала должно быть имя таблицы из которой будут браться данные. Но потом можно добавить условие «WHERE» и ещё много всего

Далее, надо добавить на форму QComboBox с которым вы будете экспериментировать. В моём случае это будет addressbookmainwindow.ui. Имя нового виджета ui->comboBox




Теперь будем заполнять этот комбобокс разными способами
ui->comboBox->setModel( new BaseComboModel( «countryname», «country», this ) );
«SELECT country.id, countryname FROM country».
Просто список стран
ui->comboBox->setModel( new BaseComboModel( «countryname», «country WHERE countrycode IN ( 'US', 'RU', 'CN' )», this ) );
«SELECT country.id, countryname FROM country WHERE countrycode IN ( „US“, „RU“, „CN“ )».
Некоторые страны выбранные по коду.
ui->comboBox->setModel( new BaseComboModel( «lastname», «persons», this ) );
«SELECT persons.id,, lastname FROM persons».
Список фамилий записанных в базе. Что бы они были, надо кликнуть кнопку «Fill test data»
ui->comboBox->setModel( new BaseComboModel( «lastname || ' — ' || email», «persons LEFT JOIN address AS a ON a.id = persons.addressid», this ) );
«SELECT persons.id, lastname || ' — ' || email FROM persons LEFT JOIN address AS a ON a.id = persons.addressid».
Список фамилий с email aдресами. Не забывайте что "||" оператор конкатенации строк только в SQLite. Для других баз понадобится переделать конкатенацию
ui->comboBox->setModel( new BaseComboModel( «lastname || ' — ' || countryname», «persons INNER JOIN address AS a ON a.id = persons.addressid INNER JOIN country AS c ON a.countryid = c.id», this ) );
«SELECT persons.id, lastname || ' — ' || countryname FROM persons INNER JOIN address AS a ON a.id = persons.addressid INNER JOIN country AS c ON a.countryid = c.id».
Список фамилий с соответствующими странами

Конечно, все эти фокусы с «JOIN» и «WHERE» выглядят интересно. Но в большинстве случаев не нужны. Поэтому и было решено использовать два параметра в конструкторе. Вместо того чтобы подавать туда SQL запрос целиком. Если вы храните все перечисления в одной таблице. И разделяете эти перечисления по какому-то дополнительному ключу. Лучше сделать третий параметр, со значением этого ключа. Вместо того чтобы использовать каждый раз «WHERE».

Повторюсь, как получить «id» выбранной записи
ui->comboBox->itemData( ui->comboBox->currentIndex(), Qt::UserRole );

Заключение




Надеюсь, несмотря на сложность кода. Вы извлекли что то полезное для себя. Если хотите узнать больше о приложении «AddressBook» приведённом здесь ради примера. Смотрите статью «Автоматизация обмена данными Qt форм с SQL базой данных».

Tags:
Hubs:
Total votes 10: ↑8 and ↓2+6
Comments23

Articles