Creating data models for QComboBox



Hello! I want to share with you in two ways how you can and should create data models for widgets like QComboBox in Qt. At the end of the article, a solution will be shown to fill the combo box from the database with one line of code.

Method number 1. Completely manual model creation


All data models in Qt must be descendants of QAbstractItemModel . Personally, in my practice, comboboxes always displayed an enumeration from an SQL database. These were gender, country, nationality and some other lists from which the user had to select one item. Therefore, when creating the model, I always had two parallel tasks:

  1. How to create human-readable item names for the user?
  2. How to connect readable items with keys that need to be written in the database?

Just in case, I’ll explain the difference, if anyone doesn’t understand. The first paragraph is the human-readable name of the paragraph. In my example, combo box for choosing nationalities. There will be words like "Russian", "Belgian", "Norwegian", etc. What the user of the program sees on the screen. The second is what the program will write to the database. Conditionally "service value". In my example, a string of the type: “russian”, “belgian”, “norwegian” is written to the database. This allows you to change the names of items that are visible to the user without unnecessary trouble. For example, they gave you the task of reducing the width of the combo box, by reducing the names of nationalities. You need to show not “Russian”, but “Rus.”. In this case, you calmly change the text displayed to the user and close the task. If you write directly to the database what is visible in the combo box. Change “Russian” -> “Rus. "Will force to write procedures for the database. In order to translate old names into new ones. That the chosen nationalities in the end-user databases would not be lost. In short, the two described names (human-readable, official) for each item. It is good practice to create supported code.


To implement the plan. First of all, you need to look at which of the QAbstractItemModel methods you must determine for yourself:

  • 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

Those. “pure virtual” methods are listed here. It would seem odd to have to implement columnCount () . Because it is obvious that the column is one. Then index () and parent () look somehow redundant, against the background of a simple linear data structure (list). They are needed more to build hierarchical tree-type models for QTreeView . Therefore, in order not to invent extra work for ourselves, it was decided to inherit the model class from QAbstractListModel , which is also suitable in our case. And it requires to implement only the last two ("pure virtual") methods from the list.


Thus, for combobox choice of nationality. It turns out the following implementation of the model:

// nationalitymodel.h
// #pragma once
#include 
class NationalityModel : public QAbstractListModel
{
	Q_OBJECT
	typedef QPair 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 ) );


All values ​​of combo box items are simply written to QList <DataPair> m_content; . And then they are issued when the combobox is called to the NationalityModel :: data () function . It is important for beginners to understand. It is not the programmer who explicitly calls this function in his code. A combo box refers to this function when it needs to! Your task is for the function to return these relevant data on request.


NationalityModel :: data () is called with two parameters. As the QAbstractItemModel :: data () prototype requires :

  • const QModelIndex & index . An object containing a line number, a column, and a link to the parent QModelIndex . Those. QComboBox reports the location (position) of the item for which data is requested. In our case, only the line number is relevant. Other parameters inside & index are only for compatibility with other models, such as QTreeView and QTableView . Therefore, our function requests a pair, "readable" and "service" values (DataPair) only for this row. Stored in a list of possible values (m_content).
  • int role . In this parameter, QComboBox reports what kind of data is needed (what role). In our case, the “readable” value is Qt :: DisplayRole , and the “service” value is Qt :: UserRole .

In one call to NationalityModel :: data () , data of one role is returned for one, specific row in the list.


If you turn to enum ItemDataRole , where Qt :: DisplayRole, Qt :: UserRole are defined . It will become clear why else you can implement such a model. For example, change the font of some items (Qt :: FontRole) . Align the text of a menu item, as in a special way. Or set a tooltip text. See the mentioned enum. Perhaps you will find there what you were looking for a long time.


Sample Source


You may be interested to learn this code at work. For these purposes, an implementation of a small address book was created .


How to download code from github.com
Initial project setup:
  1. Download the project “git clone https://github.com/stanislav888/AddressBook.git”
  2. Change the current cd AddressBook directory
  3. Initialize the git submodule init submodule
  4. Upload the submodule code to the git submodule update project
  5. Open and build a project
  6. Run the program
  7. If all is well, a window for selecting / creating a database file will appear. You can see what kind of program. For filling with test data there is a button “Fill test data”

To build, you must have QtCreator with Qt at least 5.0. Personally, I built a project with Qt 5.5.0 the gcc 5.3.1 compiler. Although the project will be collected and even run on Qt 4.8.1. To debug the database, you can use the extension for Firefox SQLite Manager .


Method number 2. Fast model creation from enumeration in SQL DB


Of course, the most correct way to organize transfers. This is to store them in the database, in the form of separate tables. And load in comboboxes in the designers of forms. And it would be ideal to have some kind of universal solution. Instead of writing separate model classes for each listing.


For implementation we need QSqlQueryModel . This is a similar model. She is also a descendant of QAbstractItemModel , but is used to display the results of a QSqlQuery SQL query in a QTableView table . In this case, our task is to adapt this class. That he would give data as in the first example.


You will be surprised, but the code turned out to be small.

// addressdialog.h
/// #pragma once
#include 
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 
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 ) );



In this implementation, QSqlQueryModel does all the work . You just need to slightly override the logic of QSqlQueryModel :: data (). First, imagine that the SQL query “SELECT country.id, countryname || '-' || countrycode FROM country . "

Of course, in the project code this is a little more intricate. But if you debug there, exactly such a line will be formed. The query displays two columns. Primary Key ("id"). And human readable values, visible in the screenshot. Since all the results of the SQL query are in Qt :: DisplayRole at QSqlQueryModel . That without changing the QSqlQueryModel , as a model of the combobox, it will simply display a list of "id". And the human-readable value will not be visible. Because the combo box does not use the second column of the model (query) in any way. You will see this if you comment out the declaration and implementation of BaseComboModel :: data ().

In order to see the list of countries, as in the screenshot, BaseComboModel :: data () :

  • returns the data of the first requested column ("id") as Qt :: UserRole of the first column
  • returns the data of the second column ("countryname || '-' || countrycode") as Qt :: DisplayRole of the first column
  • adds the string "(please select)" at the very beginning. Due to the offset numbers when requesting data from QSqlQueryModel . Those. to the results of the SQL query, the model itself adds another row

This way you can quickly and easily make models for QComboBox using BaseComboModel. For example, you have an SQL table of months in a year (“months”). Where are the two columns, "id" and "monthname". You can fill in the month selection combobox as follows:

ui-> monthsCombo-> setModel (new BaseComboModel ("monthname", "months", this));
Get the id value of the selected month ui-> monthsCombo-> itemData (ui-> monthsCombo-> currentIndex (), Qt :: UserRole); . Get the value visible to the user ui-> monthsCombo-> currentText (); . This code is much more compact than all other cases. Most developers in this situation write a separate request to the database (QSqlQuery). And then, in a loop, add the received entries to the combo box, through QComboBox :: addItem () . This is of course a working, but not the most beautiful solution.


Practice


I suspect that not everyone understood how everything works and works here. Just because this requires a very specific experience in implementing their models. In this case, so that your time for the article is not wasted. Let's just try to use the given code in practice. What would then, eventually understand it.


Two options for how to do this:

  1. Experiments based on my application - the address book mentioned above. The header and implementation of BaseComboModel is already present in the project. The examples below will be based on it.

  2. Use any other application working with SQL database. It does not have to be SQLite. Any base will do! You can simply paste the code from the listing above into the implementation file of any form.
    Of course, it would be right to make separate files, header and implementation for BaseComboModel . We assume that for now we are too lazy to do this. Of course, you will have to deal with compilation errors a bit. But they will be simple. In the tables from which you will take data for the combo box. Must be present column "id"

BaseComboModel constructor parameters (const QString & columns, const QString & queryTail, QObject * parent = 0) :

  • const QString & columns . Formation of a human-readable item name for the user. In the example above, "countryname || '-' || countrycode ” concatenation of two columns through a hyphen is applied. Concatenation operator "||" specific to SQLite. You can specify multiple columns separated by commas. But only the first will be shown.

  • const QString & queryTail . The tail of the request. The contents of the SQL query after "FROM" . Obviously, in this line, the name of the table from which the data will be taken must first be. But then you can add the WHERE clause and much more.

Next, you need to add a QComboBox to the form with which you will experiment. In my case it will be addressbookmainwindow.ui . New widget name ui-> comboBox




Now we will fill this combo box in different ways
ui-> comboBox-> setModel (new BaseComboModel ("countryname", "country", this));
"SELECT country.id, countryname FROM country . "
Just a list of countries
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 ") . "
Some countries selected by code.
ui-> comboBox-> setModel (new BaseComboModel ("lastname", "persons", this));
"SELECT persons.id ,, lastname FROM persons" .
List of surnames recorded in the database. Whatever they are, click the “Fill test data” button
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 " .
List of surnames with email addresses. Do not forget that "||" string concatenation operator in SQLite only. For other bases, you need to redo the concatenation
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 || '-' || FROM persons INNER countryname JOIN address AS a a.id = ON INNER JOIN persons.addressid country AS c = ON a.countryid c.id » .
List of surnames with relevant countries

Of course, all these tricks with "JOIN" and "WHERE" look interesting. But in most cases they are not needed. Therefore, it was decided to use two parameters in the constructor. Instead of submitting the whole SQL query there. If you store all the listings in one table. And separate these transfers by some additional key. It is better to make the third parameter, with the value of this key. Instead of using WHERE each time .

I repeat how to get the "id" of the selected record
ui-> comboBox-> itemData (ui-> comboBox-> currentIndex (), Qt :: UserRole);

Conclusion




Hopefully despite the complexity of the code. You have learned something useful for yourself. If you want to know more about the AddressBook application here for an example. See the article “Automating the exchange of data from Qt forms with SQL database”.


Also popular now: