📜 ⬆️ ⬇️

Creating data models for QComboBox



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

Method number 1. Fully manual model creation


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

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

Just in case, I will explain the difference, if someone does not understand. The first item is the human-readable item name. In my example, combobox to select nationalities. There will be words like “Russian”, “Belgian”, “Norwegian”, etc. What the user of the program sees on the screen. The second is that the program will write to the database. Conditionally "service value". In my example, a string of the following type is written to the database: “russian”, “belgian”, “norwegian”. This allows you to change the names of items visible to the user without any hassle. For example, they gave you the task to reduce the width of the combo box, at the expense of reducing the names of nationalities. You do not need to show “Russian”, but “Rus.” In this case, you can easily change the text displayed for the user and close the task. If you write directly to the database what is visible in the combo box. The changes to “Russian” -> “Rus.” Will force you to write procedures for the database. In order to translate old names into new ones. Whatever lost already selected nationality in the databases of end users. In short, the two described names (human readable, service) for each item. It is a good practice to create supported code.


To implement the plan. First of all, you need to look at which of the QAbstractItemModel methods you are required to determine:
')


Those. Listed here are "pure virtual" ("pure virtual") methods. It would seem the strangest will have to implement columnCount () . Since It is obvious that one column. Then index () and parent () look like something 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 yourself, 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 the combo box the choice of nationality. It turns out the following implementation of the model:

// 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 ) ); 


All combo box item values ​​are simply recorded in a QList <DataPair> m_content; . And then they are issued when the combobox calls on the function NationalityModel :: data () . Beginners are important to understand. No programmer explicitly calls this function in his code. And combobox refers to this function when he needs! Your task is that the function would give this actual data on request.


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


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


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


Sample source code


You may be interested to study this code in the work. For these purposes, a small address book implementation 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 directory "cd AddressBook"
  3. Initialize the git submodule init submodule
  4. You load the submodule code into the git submodule update project
  5. Open and collect 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. To fill in the test data there is a button "Fill test data"

To build, you must have QtCreator with Qt not lower than 5.0. Personally, I built a project with Qt 5.5.0 using the gcc 5.3.1 compiler. Although the project will build and even work on Qt 4.8.1. For debugging the database, you can use the extension for Firefox SQLite Manager .


Method number 2. Quickly create a model from an enumeration in a SQL database


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


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


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

 // 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 ) ); // Ie 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, all the work is done by QSqlQueryModel . You just need to override the logic of QSqlQueryModel :: data () a little . To begin with, imagine that a SQL query is written to the model “SELECT country.id, countryname || '-' || countrycode FROM country " .

Of course in the project code it is a bit more intricate. But if you debug there, such a line will be formed. The query displays two columns. Primary key ("id"). And human-readable, visible in the screenshot. Since all the SQL query results are in Qt :: DisplayRole of QSqlQueryModel . Then, without changing QSqlQueryModel , as a model of a combo box, it will simply give out the “id” list. And the human readable value will not be visible. Since Combobox does not use the second column of the model (query). 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 () :


Thus, you can quickly and easily make models for QComboBox using BaseComboModel. For example, you have a SQL table of months per year (“months”). Where two columns, "id" and "monthname". You can complete the month selection box as follows:

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


Practice


I suspect, not everyone understood how everything is arranged and works here. Just because it requires a very specific experience in the implementation of their models. In this case, what would be your time on the article was not in vain. Let's just try to use the code in practice. What then, over time, to 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 DB. 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 correct to make separate files, header and implementation for BaseComboModel . We assume that while we do it too lazy. Of course, you will have to fight a bit with compilation errors. But they will be simple. In the tables from which you will take data for combobox. The “id” column must be present .

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


Next, you need to add to the form QComboBox with which you will experiment. In my case it will be addressbookmainwindow.ui . The name of the new widget 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 names recorded in the database. What would they be, you must click the button "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 ” .
List of names with email addresses. Do not forget that "||" string concatenation operator in SQLite only. For other bases, you will need to redo concatenation.
ui-> comboBox-> setModel (new BaseComboModel ("lastname || '-' || countryname", "persons INNER JOIN address AS a ON a.id = persons.addressid INNER JOIN country AS with 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 with ON a.countryid = c.id " .
List of names with relevant countries

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

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

Conclusion




I hope, despite the complexity of the code. You have learned something useful for yourself. If you want to know more about the AddressBook application given here for the sake of example. See the article "Automating the exchange of Qt form data with a SQL database."

Source: https://habr.com/ru/post/329228/


All Articles