An introduction into Qt - Part III

published at 01.08.2013 12:35 by Jens Weller
Save to Instapaper Pocket

Welcome to Part 3 of my short introduction into Qt. This might be the last part, as my Qt Introduction course currently also spawns over 3 days. In the previous parts I gave a short overview on Qt Core followed by an introduction into QWidgets and layouts. At the end was a short overview over Qts Model View system, which I will continue in this post. Also I will give an overview on how to handle XML and SQL in Qt.

Your Model in Qt

So, I've showed what you can do do with models in the last part, in this part I'd like to give a simple example on how to implement your own model. This is always useful, when you need to expose data to the UI in Qt. This could be done in other ways too, but the Model View approach and its integration into the Widgets of Qt has clearly its advantages. Also, C++ models in Qt can later serve the data to QML Frontends, easing the porting of your application to mobile platforms for example.

So, what do we need to get started? First, you should have some sort of data to display ofc, this can be either a simple data class encapsulating the data of one Item, or a data source doing this. I usually prefer the first, sometimes the second one is better especially if there is already an existing API providing data access. I'll use as an example a small data class only containing firstname, lastname and email adress for a persons data. Next, one needs to derive from a model class in Qt, there are basically 4 Options:

I'll use QAbstractTableModel as a base class for this example, as displaying lists of people is what I'd like to do in my example app. In order to have data displayed, there are a couple of methods which need to be overwritten now:

Methodname Descriptoin
int rowCount(const QModelIndex &parent)const;

This method returns the number of rows in our model.

The ModelIndex Argument gets important for treelike models.

int columnCount(const QModelIndex &parent)const; This method returns the number of columns to display. Again the argument is for treelike models, as our model has always the same columns, the argument is ignored.
QVariant data(const QModelIndex &index, int role)const; This method returns the data at the position of the ModelIndex.
QVariant headerData(int section, Qt::Orientation orientation, int role)const; This model has to return the corresponding header names.
bool setData(const QModelIndex &index, const QVariant &value, int role); If the model is editable, this method needs to be overwritten in order to store the edited data back into the model.
Qt::ItemFlags flags(const QModelIndex &index)const; The flags method needs to be overwritten, if the model is editable, then it the implementer needs to add the editable flags there.

The question now is, how to implement those methods? A quick view on the header before I go on to the detailed implementation:

class PersonModel : public QAbstractTableModel
{
    Q_OBJECT
    std::vector<PersonalData> mydata;
public:
    typedef std::vector<PersonalData>::const_iterator const_iterator;
    explicit PersonModel(QObject *parent = 0);
    enum {FIRSTNAME=0,LASTNAME,EMAIL,MAX_COLS};

    int rowCount(const QModelIndex &parent) const;
    int columnCount(const QModelIndex &parent) const;
    QVariant data(const QModelIndex &index, int role) const;
    QVariant headerData(int section, Qt::Orientation orientation, int role) const;
void addPerson(PersonalData person); void removePerson(int row); bool setData(const QModelIndex &index, const QVariant &value, int role); Qt::ItemFlags flags(const QModelIndex &index) const; PersonalData& getPerson(size_t index); const_iterator begin()const{return mydata.begin();} const_iterator end()const{return mydata.end();} };

This is a normal model class derived from QAbstractTableModel. As I want to store the data within the model, I use std::vector for storing PersonalData objects. Q_OBJECT shows that the class is derived from QObject and the Moc will implement its features through this. The first two methods are more or less no brainers, the first returns the size of the vector, and the second MAX_COLS. That way the number of rows and cols are returned. This shows also a weakness of Qt, handling sizes as int isn't the smartest, and I wish it would be size_t or unsigned int. Lets have a look at the implementation:

The method data returns the dataitem for a certain QModelIndex, which resolves to x,y coordinates in our model:

QVariant PersonModel::data(const QModelIndex &index, int role) const
{
    if(!index.isValid())
        return QVariant();

    if(index.row() >= mydata.size() || index.row() < 0)
        return QVariant();

    if(role == Qt::DisplayRole || role == Qt::EditRole)
    {
        switch(index.column())
        {
        case FIRSTNAME:
            return mydata[index.row()].getFirstname();
        case LASTNAME:
            return mydata[index.row()].getLastname();
        case EMAIL:
            return mydata[index.row()].getEmail();
        }
    }
    return QVariant();
}

After doing some testing on the model index for being valid and in range, I ensure that data returns something useful when the model is queried in display or edit mode. The column defines which element of our data class is queried, the row relates to the index in my vector holding the data. The method headerData is implemented in the same fashion:

QVariant PersonModel::headerData(int section, Qt::Orientation orientation, int role) const
{
    if(role != Qt::DisplayRole)
        return QVariant();

    if (orientation == Qt::Horizontal)
    {
        switch (section)
        {
        case 0:
            return tr("Firstname");
        case 1:
            return tr("Lastname");
        case 2:
            return tr("Email");
        }
    }
    return QVariant();
}

In this case the model will tell a possible view querying the header names. The tr("wrapper") is for translation, so that the headers get translated in i18n. Also setData follows this pattern:

bool PersonModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
    if (index.isValid() && role == Qt::EditRole && !(index.row() >= mydata.size() || index.row() < 0))
    {
        int row = index.row();

        switch(index.column())
        {
        case 0:
            mydata[row].setFirstname(value.toString());
            break;
        case 1:
            mydata[row].setLastname(value.toString());
            break;
        case 2:
            mydata[row].setEmail(value.toString());
            break;
        default:
            return false;
        }
        emit dataChanged(index, index);
        return true;
    }
    return false;
}

After the index and the role are tested, the data gets inserted back at the correct position in the model. After the successful insertion the signal dataChanged(index,index) is emitted (send). Next, the implementation of flags is quite easy:

Qt::ItemFlags PersonModel::flags(const QModelIndex &index) const
{
    if (!index.isValid())
        return Qt::ItemIsEnabled;
    return QAbstractTableModel::flags(index) | Qt::ItemIsEditable;
}

When the index isn't valid, the value of ItemIsEnabled is returned, other wise the flags from the base class are queried and ItemIsEditable added. So, are we done yet? Um, no, as readers of my blog might already know. The methods for adding Persons and deleting them are missing. I chose to now overload the usual methods for this, but to add an interface for adding and removing Persons, as you can see in the class declarations above. Lets have a look at addPerson:

void PersonModel::addPerson(PersonalData person)
{
    if(std::find(mydata.begin(),mydata.end(),person)!=mydata.end())
        return;
    beginInsertRows(QModelIndex(),mydata.size(),mydata.size());
    /*BOOST_SCOPE_EXIT(this_){
        this_->endInsertRows();
    }BOOST_SCOPE_EXIT_END*/
    mydata.push_back(std::move(person));
    endInsertRows();
}

The first row has the purpose to keep the data unique. Then beginInsertRows gets called, this function tells the model implementation in Qt, that we are about to add data to the model. I'm feeling fancy and use move + push_back, ofc. emplace would also be an option. Then, endInsertRows is called, which isn't the best design, as exceptions will block this call if thrown before. Thats why actual code here could use BOOST_SCOPE_EXIT, as the code demonstrates. Unfortunately, this is a Qt training, so adding boost as an extra dependency isn't liked by all my clients. And removePerson is similar:

void PersonModel::removePerson(int row)
{
    beginRemoveRows(QModelIndex(),row,row);
    /*BOOST_SCOPE_EXIT(this_){
        this_->endRemoveRows();
    }BOOST_SCOPE_EXIT_END//*/
    mydata.erase(std::next(mydata.begin(),row));
    endRemoveRows();
}

I've chosen to delete the row by the index, so in order to get the iterator for erase, one could use std::advance. But C++11 also offers us for this std::next, making it a lot easier. But before doing this, the Qt Model architecture requires to call beginRemoveRows as shown. And afterwards endRemoveRows needs to be called. BOOST_SCOPE_EXIT applies as above.

Still, this is only a simple example. You can customize the display of a row with a delegate, implement your own views etc. Also Qt offers further standard models as for working with XML or SQL as input.

Qt & XML

Qt used to have its own module for XML, Qt Xml, but with Qt5 this became deprecated. Xml handling in Qt5 is supposed to be done with the Xml-Stream reader and writer classes. Which already also existed in Qt4. Personally, I don't use a lot of XML, so this seems fine. But especially reading XML with the stream reader seems sometimes painfull. Writing is quite easy. I'll use the code from my example Project to show how to write and read with QXmlStreamWriter/Reader, starting with writing:

QString path = QFileDialog::getSaveFileName(this,"Datei Speichern");
QFile file(path);
if(!file.open(QFile::WriteOnly|QIODevice::Text))
    return;
QXmlStreamWriter writer(&file);

writer.setAutoFormatting(true);
writer.writeStartDocument();
writer.writeStartElement("teilnehmer");
PersonModel::const_iterator it = model.begin(),end = model.end();
for(;it != end;++it)
{
    writer.writeStartElement("person");
    writer.writeTextElement("firstname",it->getFirstname());
    writer.writeTextElement("lastname",it->getLastname());
    writer.writeTextElement("email",it->getEmail());
    writer.writeEndElement();
}
writer.writeEndElement();
writer.writeEndDocument();

So, I simply iterate over my model, write out the different items as XML Nodes and the job is done. I could also write other XML elements such as comments or attributes. Reading XML is possible through QXmlStreamReader, which operates direclty on the token stream of the XML Parser. Its your job to track the position and level of your xml file. Reading is quite easy in this simple example:

QString path = QFileDialog::getOpenFileName(this,"Datei Speichern");
QFile file(path);
if(!file.open(QFile::ReadOnly|QIODevice::Text))
    return;
QXmlStreamReader reader(&file);

while(!reader.atEnd())
{
    if(reader.name() != "person")
        reader.readNextStartElement();
    if(reader.name() == "person")
    {
        qDebug() << reader.name();
        QString firstname,lastname,email;
        while(reader.readNextStartElement())
        {
            QStringRef name = reader.name();
            if(name == "person")
                break;
            if(name == "firstname")
                firstname = reader.readElementText();
            else if(name == "lastname")
                lastname = reader.readElementText();
            else if(name == "email")
                email = reader.readElementText();
        }
        if(!firstname.isEmpty() && !lastname.isEmpty() && !email.isEmpty())
            model.addPerson(PersonalData(firstname,lastname,email));
    }
}

Reading smaller XML formats is quite simple with this, for more complex formats I would wrap the StreamReader in a class doing some of the work for you. Also writing code like this always brings lots of boilerplate code with it.

The Qt XML module from 4.x contained a SAX/DOM Parser approach and required often to implement you own handler classes. Qt also has a XML Pattern module, which can handle XQuery, XPath, XSLT and XML Schemas for you.

Qt & SQL

Qt offers build in SQL support for a lot of databases. The Qt SQL API has 3 main layers: driver layer, SQL API Layer, User Layer. The first layer is only interesting, when you need to write your own database drivers, which Qt already brings a lot, so normally you deal more with the SQL API and the User Layer. The User Layer consists of Qts SQLModel classes which can display SQL Data into Qts Model/View system. For this introduction I'd like to focus on the SQL API layer, as this is the part connecting you to databases, which lets you do SQL, save and load data.

The SQL API layer consists of classes needed to connect to a database, and do queries. QSqlDatabase and QSqlQuery are the two main classes, with which you usually have to deal. A third important class is QSqlError.

In my example application I have a central class dealing with the database work for my model, adding loading and saving the data to a SQLite Database. Lets have a look at the code. The first thing to do is selecting and opening the database, and ensure that the correct datastructre (aka tables) exist:

SQLBackingStore::SQLBackingStore(const QString& database, const QString& db_type)
{
    if(openDB(database,db_type))
    {
        QSqlQuery query;
        query.exec("CREATE TABLE IF NOT EXISTS person("
                    "id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
                    "lastname VARCHAR(50) NOT NULL,"
                       "firstname VARCHAR(50),"
                       "email VARCHAR(100),"
                       "pic VARCHAR(15)    );");
        if(query.lastError().isValid())
            QMessageBox::critical(0,"Database error",query.lastError().text());
    }
    else
        QMessageBox::critical(0,"Database error","could not open DB");
}

bool SQLBackingStore::openDB(const QString &database, const QString &db_type)
{
    db = QSqlDatabase::addDatabase(db_type);
    db.setDatabaseName(database);
    if(db.open())
        return true;
    return false;
}

This example already shows the basic usage of SQL in Qt. db is a member of the class, with QSqlDatabase as type. Opening a connection and executing SQL. The QSqlQuery class can also use prepared queries to insert data into the database:

bool SQLBackingStore::createPerson(PersonalData& person)
{
    QSqlQuery query(db);
    query.prepare("INSERT INTO person(firstname,lastname,email) VALUES(:firstname,:lastname,:email)");
    query.bindValue(":firstname",person.getFirstname());
    query.bindValue(":lastname",person.getLastname());
    query.bindValue(":email",person.getEmail());
    if(!query.exec())
    {
        QMessageBox::critical(0,"Database error",query.lastError().text());
        qDebug() << query.lastQuery();
    }
    else
    {
        person.setLocalId(query.lastInsertId().toInt());
        return true;
    }
    return false;
}

The syntax of ":name" allows to replace with values through bindValue later. This will also do the sanitizing for you. Often you like to know the id of a dataset after inserting into the database, in order to easily refer later to it. This can be queried with lastInsertId as shown above. Also QSqlDatabase allows the use of transactions, so when saving all your data to the database, transactions can not only secure that your data gets through, but also speed your code up. In my boost dependency analyzer tool saving to a SQLite database took several minutes, with wrapping in transactions its down to 20-30 seconds. In this case the result is a 4.5 mb database.

There is still a lot of Qt left which I couldn't show in this 'short' introduction, I might continue with Drag&Drop in Part 4.

 

Join the Meeting C++ patreon community!
This and other posts on Meeting C++ are enabled by my supporters on patreon!