Всем привет! Хочу поделиться с вами двумя способами, как можно и нужно создавать модели данных для виджетов типа 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 > 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 базой данных».

Поделиться с друзьями
-->

Комментарии (23)


  1. dfm
    22.05.2017 19:41

    Что-то не собирается

    ошибка
    ошибка: no matching function for call to ‘BaseComboModel::BaseComboModel(QString, const char [8], AddressBookMainWindow*)’
    #define NEW_COUNTRY_MODEL new BaseComboModel( QString( "%1 || ' — ' || %2 " ).arg( AddressBookMainWindow::COUNTRY_NAME_COLUMN ).arg( AddressBookMainWindow::COUNTRY_CODE_COLUMN ), «country», this )


    1. stanislav888
      23.05.2017 22:11

      Починил, пробуйте.


  1. iCpu
    23.05.2017 06:18
    +3

    Добавление пустых строчек-заменителей делается всё на стандартных моделях без зауми. QSqlQueryModel

    SELECT -1 as id, '(Select country)' as countryname UNION SELECT id, countryname FROM country
    

    В чём магия создания моделей именно для QComboBox — не раскрыто.
    Как вставлять отличные от QListView отображения в QComboBox и пользовать их — не сказано.

    Иными словами, много и ни о чём.


    1. stanislav888
      23.05.2017 17:54

      Добавление пустых строчек-заменителей делается всё на стандартных моделях без зауми. QSqlQueryModel

      Пустые строчки можно делать и SQL запросом, согласен. Хотя если брать ваш UNION. То как быть в случае когда второй столбец будет иметь какой то другой тип данных? Переписать запрос?! В том то и дело что модели всё равно на конфликт типов данных с SQL запросом.
      В чём магия создания моделей именно для QComboBox — не раскрыто.

      В том то и дело что тут нет никакой магии. Всё проще простого. Смотрите код. Причём модель для таблицы можно подать в комбобокс и наоборот.
      Как вставлять отличные от QListView отображения в QComboBox и пользовать их — не сказано.

      А я обещал осветить именно эту тему?! Я обещал заполнять контентом комбобоксы одной строчкой кода. И показал.
      Решается это так: QTableView c указанной QSqlQueryModel. Но там надо назначить делегаты которые будут отображать комбобоксы при редактировании. Эти делегаты обычно пишут наследуя от QStyledItemDelegate. Это большая тема. Я не смогу описать здесь все тонкости за раз.


      1. iCpu
        24.05.2017 07:37

        То как быть в случае когда второй столбец будет иметь какой то другой тип данных?
        Писать модель или кастовать их в общий тип на стороне БД. А у вас часто так бывает, что типы в столбце не совпадают?
        В том то и дело что тут нет никакой магии.
        Я с вами не согласен. Есть некоторые фишки, которые есть только у QComboBox, и которые можно интересно использовать: смена отображения, редактирование с валидатором, манипуляции над моделью аля мультивыбор. Но вы о них ни слова не сказали, как будто даже доки не открывали.
        Я обещал заполнять контентом комбобоксы одной строчкой кода. И показал.
        Вы целую модель под это написали. А всего-то надо было запихнуть union в запрос и modelColumn(1) в комбобокс, да IDшник брать по comboBox->model()->index(index.row(),0).data().toInt().
        Решается это так
        Что решается? Это уже решено, скрин по ссылке. Стоило хотя бы просто указать, что вместе с моделью можно менять и отображение.


        1. stanislav888
          24.05.2017 13:48

          Писать модель или кастовать их в общий тип на стороне БД. А у вас часто так бывает, что типы в столбце не совпадают?

          У меня так может быть. У моих коллег такое может случиться. Поэтому мне надо писать поддерживаемый код. Который не свалиться на таких мелочах. К тому же это учебный пример. О том что так можно делать в принципе. Я набегался по этим граблям с UNION потому что мой запрос к базе обязан был работать аж в двух базах одновременно. Мне совсем не охота гадать кто и когда наступит на эти грабли снова.
          Я с вами не согласен. Есть некоторые фишки, которые есть только у QComboBox, и которые можно интересно использовать: смена отображения, редактирование с валидатором, манипуляции над моделью аля мультивыбор. Но вы о них ни слова не сказали, как будто даже доки не открывали.

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

          Просто для меня это естественно. И я не считаю это чем то сложным и заумным.
          А всего-то надо было запихнуть union в запрос и modelColumn(1) в комбобокс, да IDшник брать по comboBox->model()->index(index.row(),0).data().toInt().

          Даже modelColumn(1) вам тут не нужен. Надо просто первым столбцом выводить не первичный ключ. А пользовательские данные. Например «SELECT countryname, id FROM countries».
          Что решается? Это уже решено, скрин по ссылке.

          Менять данные таблицы в полях слева от таблицы?! Ваш проект вы и решаете что тут считать решением.
          Стоило хотя бы просто указать, что вместе с моделью можно менять и отображение.
          Напишите свою статью о комбобоксах. Я за вас только порадуюсь.


          1. iCpu
            24.05.2017 18:46

            Извините, что посмел лезть к Его Величеству со своими ничтожнейшими высирами! Конечно, Вы правы! Повелите мне самолюстрироваться-с?


  1. DaylightIsBurning
    23.05.2017 15:16

    существуют ли хорошие реализации QCOmboBox с checkboxes? Что бы можно было выбрать более одного пункта и удобно было выбрать все/ни одного?


    1. reishi
      23.05.2017 17:27

      По-моему для такого больше подойдет QListView и отдельные кнопки выбрать все/сбросить.
      Но если очень надо, то берите любой вариант из гугла «qcombobox with checkboxes» и просто добавляйте контекстное меню с соответствующими командами.


      1. DaylightIsBurning
        23.05.2017 18:08

        Понятно, спасибо. Я уже использую одну из реализаций из гугла, но все что я пробовал не без недостатков.


    1. stanislav888
      24.05.2017 14:00

      Не работал ни разу с такими. Можно копнуть в сторону создания своего делегата для QComboBox. QComboBox::setItemDelegate(QAbstractItemDelegate * delegate). Если он похож как для QTableView. Там хоть преферанс с куртизанками впихнуть можно. Но это тема требующая времени.


  1. granindb
    23.05.2017 17:58

    Я в свое время написал для себя класс выпадающего списка для работы с данными пита [id,text]

       comboBox1 = new BaseKeyComboBox();
        comboBox1->setModel(sourceModel); //модель примерно такая "SELECT RecordID, DisplayText FROM MyCatalog;"
        comboBox1->setModelColumn(1); // Колонка для отображения текста
        comboBox1->setKeyColumn(0); // Колонка для ключа ID ТОЛЬКО int!!!!!!!
        comboBox1->setEditable(false);
    
        // mapper - это QDataWidgetMapper
        // Подключается как обычно
        mapper->addMapping(comboBox1, mainModel->fieldIndex("referemceid"));
    

    Причем я не понял зачем делать модель специально для этого случая.
    У меня можно подцепить любую.

    И я не увидел, он у вас с QDataWidgetMapper умеет работать?


    1. stanislav888
      23.05.2017 18:07

      Причем я не понял зачем делать модель специально для этого случая.

      Вот вы заполняете комбобокс пятью строчками кода плюс незнаю что там в модели. А я это делаю одной строкой. Когда комбобоксов от 10-и в программе. Есть над чем задуматься.
      И я не увидел, он у вас с QDataWidgetMapper умеет работать?

      Нет. Дело в том что мой комбобокс обязан устанавливать значения по вторичному ключу БД из Qt::UserRole. А QDataWidgetMapper работает со строкой из Qt::DisplayRole. К тому же этот маппер работает через раз.
      Если у вас есть пара часов свободного времени. Можете ознакомиться как этот комбобокс сохраняет и заполняет значения из БД — https://habrahabr.ru/post/328670/
      Грубо говоря я написал свою замену QDataWidgetMapper


      1. granindb
        24.05.2017 09:00

        Вот вы заполняете комбобокс пятью строчками кода плюс незнаю что там в модели. А я это делаю одной строкой. Когда комбобоксов от 10-и в программе. Есть над чем задуматься.

        Я могу заполнять и одной строчкой кода, если все остальное используется по умолчанию. А модель самая обычная, самое главное чтобы там были столбцы типа int для ключа БД и QString для отображения.
        А если таких виджетов много, то можно и фабрику под это дело использовать. Это уже вопрос уровня абстракций.

        QDataWidgetMapper и должен работать с тем, что показывается пользователю. Никто же не делает несколько пунктов с одинаковым содержимым 'Пункт1, Пункт1', или я смысл не уловил…
        И он у меня работает и глюков пока не замечал.


        1. stanislav888
          24.05.2017 13:01

          Я могу заполнять и одной строчкой кода, если все остальное используется по умолчанию. А модель самая обычная, самое главное чтобы там были столбцы типа int для ключа БД и QString для отображения.
          А если таких виджетов много, то можно и фабрику под это дело использовать. Это уже вопрос уровня абстракций.

          Насколько я понимаю вы сделали свою реализацию QComboBox — BaseKeyComboBox. Он использует простую модель типа QSqlQueryModel. Я использую простой QComboBox. Но сделал для него особенную модель из QSqlQueryModel.
          В обоих случаях нам надо добавлять в проект два лишних файла. С примерно одинаковым количеством кода. И в обоих случаях это даёт заполнение QComboBox данными одной строчкой кода.
          Теперь о том чего нельзя сделать в вашем варианте. Если посмотреть шире то модель она не только для данных. Через наследника модели можно определить всплывающую подсказку(tooltip), шрифт пункта и ещё много чего перечисленного в enum ItemDataRole (qnamespace.h).
          В частности, хотел в своё время запрашивать из базы три столбца вместо двух. Что бы третий выводился как ToolTipRole первого столбца модели. Именно поэтому у меня название параметра columns в BaseComboModel.
          QDataWidgetMapper и должен работать с тем, что показывается пользователю. Никто же не делает несколько пунктов с одинаковым содержимым 'Пункт1, Пункт1', или я смысл не уловил…

          Может я не так объяснил. Попробую по другому.


          1. granindb
            24.05.2017 13:14

            Ладно, пусть это будет делом вкуса, так сказать.
            Но…

            То без изменения QSqlQueryModel, в качестве модели комбобокса, выдаст просто список «id». А человекочитаемое значение не будет видно

            Это же явное заблуждение, которое и привело вас к созданию собственной модели.
            Есть же void setModelColumn(int visibleColumn), которое и задает какой столбец выводить пользователю.


        1. stanislav888
          24.05.2017 13:11

          (продолжение)
          Кода вы работаете с комбобоксоом. Вам важнее какой вторичный ключ записать в базу. А не какое название выбрал юзер на экране. Допустим комбобокс выбора стран. Пользователь может выбрать там «Russia», но вам ведь не название страны надо записывать в базу в результате. А «id» этой записи в cтолбец «countryid». И в моём случае я легко получаю этот «id» из UserRole. QDataWidgetMapper не может менять именно вторичные ключи в базе.


          1. granindb
            24.05.2017 13:20

            А вот для этого у меня есть

            Q_PROPERTY(int currentKey READ currentKey WRITE setCurrentKey NOTIFY currentKeyChanged USER true)
            

            которое и позволяет QDataWidgetMapper выполнять свою работу.

            И, да… У меня записывается в базу именно id
            В первом примере кода это видно
            mapper->addMapping(comboBox1, mainModel->fieldIndex("referemceid"));
            

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


            1. stanislav888
              24.05.2017 14:35

              Полностью согласен. Cохранять данные через QDataWidgetMapper гораздо красивее. Ваш вариант с Q_PROPERTY конечно хорош.
              Если ориентироваться на QDataWidgetMapper, можно ещё задать ему делегата. Который, внезапно, отвечает как раз за то чтобы взять значение у произвольного виджета и превратить его в QVariant().
              Просто у меня QDataWidgetMapper не завёлся. И я подумал что очередные пляски с бубном как с QSqlTableModel мне не нужны.


              1. granindb
                24.05.2017 15:29

                И я подумал что очередные пляски с бубном как с QSqlTableModel мне не нужны.

                Это вы про
                QSqlTableModel может сохранять данные в базу. Странно что на форме раздельное сохранение данных для таблицы и простых полей. QSqlTableModel иногда работает с ошибками. Потом, у него немного ограниченный функционал. Поэтому вы можете плюнуть и воспользоваться QSqlQueryModel. Который уже ничего не сохраняет, но и ограничений у него нет. В этом случае таблица будет только для чтения.
                ?

                Если да, то я опять же написал EditSqlQueryModel
                С которой можно делать так…
                contentModel = new EditSqlQueryModel(db, this);
                        contentModel->setEditStrategy(EditSqlQueryModel::OnManualSubmit);
                        contentModel->showDeleted(false);
                        contentModel->setQuery("SELECT "
                                               "a.recordid, "
                                               "a.productionid, "
                                               "a.productionclassid, "
                                               "d.name as productionclassname, "
                                               "FROM foamcontourproductioncontent a "
                                               "left join productionclass as d on a.productionclassid = d.recordid "
                                               "where a.foamcontourproductionid = " + QString::number(m_recordId) + " "
                                               "order by a.recordid;");
                
                        contentModel->setTable("foam.productioncontent");
                        contentModel->init();
                
                        EditSqlQueryModel::FieldMap fMap;
                        QSqlRecord rec;
                        rec = contentModel->record();
                        fMap.insert("recordid", rec.field("recordid"));
                        fMap.insert("productionid", rec.field("productionid"));
                
                        contentModel->setUpdatableFields(fMap);
                

                Такая реализация по мотивам Visual FoxPro.
                Она правда имеет мало функционала по сравнению с CursorAdapter из VFP, но мне хватает.

                Вы по-моему не с той стороны подошли к проблеме.


                1. stanislav888
                  24.05.2017 17:11

                  Нет, тут немного о другом. Я написал велосипед для обмена данными формы с базой. И для пущей наглядности усложнил его таблицей слева.
                  Поскольку решение это примерное. Демонстрация подхода. Который можно применить на любых формах. Т.е. и на формах без левой таблицы. А раз так, мне надо заполнять таблицу и форму отдельно.
                  Моя нота выше. Про то что программист может по своему желанию сменить модель для левой таблицы на другую. Вдруг заглючит, что бывало. Или функционал окажется недостаточным. Или она вообще будет не нужна.

                  Судя по вашему коду. Вы написали примерно такой же велосипед как у меня в той статье.


                1. stanislav888
                  24.05.2017 17:21

                  «Пляски с бубном» вокруг QSqlTableModel у меня были на другом проекте, в продакшене. Здесь просто констатация факта что иногда глючит. Поэтому надо оставлять место для запасных вариантов типа «QSqlQueryModel».


  1. granindb
    24.05.2017 15:26

    Не туда ответил