Wiki

Mapping Data to Widgets

by David Boddie

The QDataWidgetMapper class, introduced in Qt 4.2, provides an interface that allows data from a model to be mapped to widgets on a form. This widget-centric approach to displaying data makes it easier to create record-based applications, and allows the user interface to be customized with familiar tools such as Qt Designer.

Although Qt's item view widgets are capable of displaying large quantities of data, many applications tend to use record or form-based user interfaces in order to present simplified views onto data. Sometimes this is because users are familiar with a record-based interface, or it can be a consequence of the way the information is stored.

In this article we will show how to use QDataWidgetMapper to create a simple record-based user interface, examining the way it interacts with other components in Qt's model/view framework, and briefly look at how we can use it to access a SQL database.

The code for the examples in this article can be found on the Qt Quarterly website.

A Simple Widget Mapper

QDataWidgetMapper class is a helper class that is designed to access items of data in a table model, displaying the information obtained in a collection of registered widgets.

In its default configuration, a QDataWidgetMapper object accesses a single row at a time, and maps the contents of specific columns to specific widgets. For each row it examines, it takes data from each column and writes it to a property in the corresponding widget, as the following diagram shows.

Simple-Mapping

Let's take a look at some example code which creates a simple form-based user interface, using a QDataWidgetMapper object and a simple table model to update the current record whenever the user clicks a push button.

The example consists of a single Window class:

class Window : public QWidget
{
    Q_OBJECT
 
public:
    Window(QWidget *parent = 0);
 
private slots:
    void updateButtons(int row);
 
private:
    void setupModel();
    ...
    QStandardItemModel *model;
    QDataWidgetMapper  *mapper;
};

The class provides a slot to keep the user interface consistent and a private function to set up a model containing some data to display.

Almost everything is set up in the constructor of the Window class. We start by initializing the model containing the data we want to show (we'll look at this in more detail later) and put the user interface together:

Window::Window(QWidget *parent)
    : QWidget(parent)
{
    setupModel();
    nameLabel = new QLabel(tr("Na&me:"));
    nameEdit = new QLineEdit();
    addressLabel = new QLabel(tr("&Address:"));
    addressEdit = new QTextEdit();
    ageLabel = new QLabel(tr("A&ge (in years):"));
    ageSpinBox = new QSpinBox();
    nextButton = new QPushButton(tr("&Next"));
    previousButton = new QPushButton(tr("&Previous"));

Only the editable widgets will be used with the QDataWidgetMapper. The buttons allow the user to move between records.

Using the mapper itself is trivial; we construct a QDataWidgetMapper instance, provide a model for it to use, and map each widget to a specific column in the model:

mapper = new QDataWidgetMapper(this);
mapper->setModel(model);
mapper->addMapping(nameEdit, 0);
mapper->addMapping(addressEdit, 1);
mapper->addMapping(ageSpinBox, 2);

We will need to make sure that the model contains the correct data in each column, but setting up the mapper is rarely much more complicated than this.

To make the example interactive, we connect the push buttons to the mapper so that the user can examine each record in turn. For consistency, the mapper is connected to the updateButtons() slot, so that we can enable and disable the push buttons as required.

    connect(previousButton, SIGNAL(clicked()),
            mapper, SLOT(toPrevious()));
    connect(nextButton, SIGNAL(clicked()),
            mapper, SLOT(toNext()));
    connect(mapper, SIGNAL(currentIndexChanged(int)),
            this, SLOT(updateButtons(int)));
    mapper->toFirst();
}

Once all the connections are set up, we configure the widget mapper to refer to the first row in the model.

To show the simplest possible use of QDataWidgetMapper, we use a QStandardItemModel as a ready-made table model. The setupModel() function creates a fixed-size model and initializes it using some data prepared using three QStringLists:

void Window::setupModel()
{
    model = new QStandardItemModel(5, 3, this);
 
    QStringList names;
    names << "Alice" << "Bob" << "Carol"
          << "Donald" << "Emma";
 
    // We set up the addresses and ages here.
 
    for (int row = 0; row < 5; ++row) {
      QStandardItem *item = new QStandardItem(names[row]);
      model->setItem(row, 0, item);
      item = new QStandardItem(addresses[row]);
      model->setItem(row, 1, item);
      item = new QStandardItem(ages[row]);
      model->setItem(row, 2, item);
    }
}

We store the names, addresses, and ages of a group of people in columns 0, 1, and 2 respectively, with each row containing the information for a specific person. The columns used are the same as those we specified to the mapper, so all names will be shown in the QLineEdit, all addresses in the QTextEdit, and all ages in the QSpinBox.

The updateButtons() slot is called whenever the mapper visits a row in the model, and is simply used to enable and disable the push buttons:

void Window::updateButtons(int row)
{
    previousButton->setEnabled(row > 0);
    nextButton->setEnabled(row < model->rowCount() - 1);
}

When the example is run, the buttons let the user select different records by changing the mapper's current row in the model. Since we used editable widgets and an writable model, any changes made to the information shown will be written back to the model.

Using Delegates to Offer Choices

For widgets that only hold one piece of information, like QLineEdit and the others used in the previous example, using a widget mapper is straightforward. However, for widgets that present choices to the user, such as QComboBox, we need to think about how the list of choices is stored, and how the mapper will update the widget when a choice is made.

Let's look at the previous example. We'll use almost the same user interface as before, but we'll replace the QSpinBox with a QComboBox. Although the combo box could display values from a specific column in the table model, there's no way for the model to store both the user's current choice and all the possible choices in a way that is understood by QDataWidgetMapper.

We could set a different model on the combo box in order to provide a choice to the user. However, this change alone will simply cause the combo box to become detached from the widget mapper. To manage this relationship, we need to introduce another common model/view component: a delegate.

Combo-Mapping

The above diagram shows what we want to achieve: A combo box is used to display a list of choices for each item in the third column of the model. To manage this, we create a separate list model containing the possible choices for the combo box ("Home", "Work" and "Other"). In the third column of the table model, we reference these choices by their row numbers in the list model.

Basing this example on the previous one, we use a combo box instead of a spin box, configure it to use a list model containing the choices we want to make available to the user, and construct a custom delegate for use with the widget mapper.

The relevant part of the Window class's constructor now looks like this:

setupModel();
...
typeComboBox = new QComboBox();
typeComboBox->setModel(typeModel);
 
mapper = new QDataWidgetMapper(this);
mapper->setModel(model);
mapper->setItemDelegate(new Delegate(this));
mapper->addMapping(nameEdit, 0);
mapper->addMapping(addressEdit, 1);
mapper->addMapping(typeComboBox, 2);
...

When we set up the table model in the setupModel() function, we also initialize the private typeModel member variable using a QStringListModel to hold the list of choices for the combo box:

void Window::setupModel()
{
    QStringList items;
    items << tr("Home") << tr("Work") << tr("Other");
    typeModel = new QStringListModel(items, this);
 
    // Set up the table model.
}    

Now, let's look at the delegate. The Delegate class is derived from the QItemDelegate class, and provides a minimal implementation of the API to interpret data specifically for combo boxes:

class Delegate : public QItemDelegate
{
    Q_OBJECT
public:
    Delegate(QObject *parent = 0);
    void setEditorData(QWidget *editor,
                       const QModelIndex &index) const;
    void setModelData(QWidget *editor,
                      QAbstractItemModel *model,
                      const QModelIndex &index) const;
};

Since the delegate only needs to communicate information between the model and the widget mapper, we only need to provide implementations of the setEditorData() and setModelData() functions. In many situations, delegates are responsible for creating and managing editors, but this is unnecessary when they are used with QDataWidgetMapper because the editors already exist.

The setEditorData() function initializes editors with the relevant data. To make the code a little more generic, we don't explicitly check that the editor is an instance of QComboBox, but instead check for both the absence of a user property and the presence of a "currentIndex" property.

void Delegate::setEditorData(QWidget *editor,
                       const QModelIndex &index) const
{
  if (!editor->metaObject()->userProperty().isValid()) {
    if (editor->property("currentIndex").isValid()) {
      editor->setProperty("currentIndex", index.data());
      return;
    }
  }
  QItemDelegate::setEditorData(editor, index);
}

We set the property to the value referred to by the model index, changing the currently visible item in the combo box as a result. We fall back on the base class's implementation for all other widgets.

The setModelData() function is responsible for transferring data made available in editors back to the model. If the editor has no valid user property, but has a "currentIndex" property, its value is stored in the model.

void Delegate::setModelData(QWidget *editor,
                     QAbstractItemModel *model,
                     const QModelIndex &index) const
{
  if (!editor->metaObject()->userProperty().isValid()) {
    QVariant value = editor->property("currentIndex");
    if (value.isValid()) {
      model->setData(index, value);
      return;
    }
  }
  QItemDelegate::setModelData(editor, model, index);
}

In all other cases, the base class's implementation is used to marshal data between the editor and the model.

Note that, in both functions, we do not obtain any information about the actual data shown in the combo box, since that is already stored in a separate model.

Preferred Properties

In order to transfer data between models and widgets, each QDataWidgetMapper needs to know which widget properties to access. Many widgets have a single user property that can be obtained using the userProperty() function of the relevant meta-object, and this is automatically used when it is available.

Problems arise when a widget you want to use doesn't have a user property, as is the case with QComboBox. To solve this problem, QDataWidgetMapper in Qt 4.3 will have a new addMapping() overload that accepts a property name, making it possible to override the mapper's default behavior and enable widgets without user properties to be used.

Mapping Information from a Database

One of the most common uses of QDataWidgetMapper is as a view onto a collection of database records, stored as rows in a table. Since the class itself is designed to work with any correctly-written model, creating another example like the first one in this article would be trivial. However, it turns out that record-based applications often require the user to select a value from a fixed set of choices &endash; see the Qt 4 Books demo for an example &endash; making it a more interesting case to explore.

In a typical SQL database application, the two models we used in the previous example would be represented as two tables in a database. We'll create a "person" table containing the names and addresses, and use foreign keys into a table containing the address types. The table has the following layout:

idnameaddresstypeid
1Alice123 Main Street101
2BobPO Box 32 ...102
3CarolThe Lighthouse ...103
4Donald47338 Park Avenue ...101
5EmmaResearch Station ...103

The "typeid" values refer to values in the "id" column of the "addresstype" table, which has this layout:

iddescription
101Home
102Work
103Other

In the setupModel() function, we set up a single QSqlRelationalTableModel instance, using a SQLite in-memory database, populated using a series of SQL queries (not shown here):

QSqlDatabase db = QSqlDatabase::addDatabase("<a href="http://www.crossplatform.ru/documentation/qtdoc4.3/qsqlite.php">QSQLITE</a>");
db.setDatabaseName(":memory:");
 
// Set up the "person" and "addresstype" tables.
 
model = new QSqlRelationalTableModel(this);
model->setTable("person");

The "person" table contains the information we want to expose to the widget mapper, so we use setTable() to make it the table operated on by the model.

We need to set up the relation between the two tables. Instead of using a hard-coded value for the "typeid" column number, we ask the model for its field index, and record it for later use:

typeIndex = model->fieldIndex("typeid");
model->setRelation(typeIndex,
       QSqlRelation("addresstype", "id", "description"));
model->select();

The relation specifies that values found in the "typeid" column are to be matched against values in the "id" column of the "addresstype" table, and values from the "description" column are retrieved as a result.

In the Window constructor, we obtain the model used to handle this relation, using the previously-stored field index, so that we can use it with the combo box, just as in the previous example. We use the setModelColumn() function to ensure that the combo box displays data from the appropriate column in the model:

QSqlTableModel *rel = model->relationModel(typeIndex);
typeComboBox->setModel(rel);
typeComboBox->setModelColumn(
              rel->fieldIndex("description"));
 
mapper = new QDataWidgetMapper(this);
mapper->setModel(model);
mapper->setItemDelegate(new QSqlRelationalDelegate(this));
...
mapper->addMapping(typeComboBox, typeIndex);

Setting up the widget mapper is the same as before, except that we use a QSqlRelationalDelegate to handle the interaction between the relational model and the mapper.

Aside from the SQL initialization, this example is more or less the same as the previous one; the QSqlRelationalDelegate behaves differently to our custom Delegate class behind the scenes, but performs the same basic role.

Summary

QDataWidgetMapper allows us to map specific columns (or rows) in a table model to specific widgets in a user interface, allowing the user to access data in discrete record-like slices.

Fields containing choices that need to be selected from a fixed set of values require some special handling. We use delegates to mediate between the model and an additional resource containing the available choices. We also need to tell editors like QComboBox to display these choices rather than raw data from the model.

The QtSql module classes provide the QSqlRelationalDelegate class to help deal with SQL models, but we need to take care to set up relations between tables correctly.


Copyright © 2007 Trolltech Trademarks