Mapping Data to Widgets

Материал из Wiki.crossplatform.ru

Перейти к: навигация, поиск
Image:qt-logo_new.png Image:qq-title-article.png
Qt Quarterly | Выпуск 21 | Документация

by David Boddie

The QDataWidgetMapper class, introduced in Qt 4.2, provides an interface thatallows data from a model to be mapped to widgets on a form. This widget-centricapproach to displaying data makes it easier to create record-basedapplications, and allows the user interface to be customized with familiartools such as Qt Designer.

Содержание

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

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

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

[править] A Simple Widget Mapper

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

In its default configuration, a QDataWidgetMapper object accesses asingle row at a time, and maps the contents of specific columns to specificwidgets. For each row it examines, it takes data from each column andwrites it to a property in the corresponding widget, as the followingdiagram shows.

center

Let's take a look at some example code which creates a simple form-baseduser interface, using a QDataWidgetMapper object and a simple tablemodel 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 privatefunction 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 toshow (we'll look at this in more detail later) and put the user interfacetogether:

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 QDataWidgetMapperinstance, provide a model for it to use, and map each widget to a specificcolumn 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 eachcolumn, but setting up the mapper is rarely much more complicated than this.

To make the example interactive, we connect the push buttons to the mapperso that the user can examine each record in turn.For consistency, the mapper is connected to the updateButtons() slot, sothat 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 referto 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 dataprepared 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 specificperson. 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 inthe 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 recordsby changing the mapper's current row in the model. Since we used editablewidgets and an writable model, any changes made to the informationshown will be written back to the model.

[править] Using Delegates to Offer Choices

For widgets that only hold one piece of information, like QLineEditand the others used in the previous example, using a widget mapper isstraightforward. However, for widgets that present choices to the user, suchas 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 interfaceas before, but we'll replace the QSpinBox with a QComboBox. Althoughthe 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 allthe 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 choiceto the user. However, this change alone will simply cause the combo box tobecome detached from the widget mapper. To manage this relationship, we needto introduce another common model/view component: a delegate.

center

The above diagram shows what we want to achieve: A combo box is used todisplay 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 possiblechoices for the combo box ("Home", "Work" and "Other"). In the thirdcolumn of the table model, we reference these choices by their row numbersin the list model.

Basing this example on the previous one, we use a combo box instead of aspin box, configure it to use a list model containing the choices we want tomake available to the user, and construct a custom delegate for use with thewidget 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 alsoinitialize the private typeModel member variable using aQStringListModel 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 fromthe QItemDelegate class, and provides a minimal implementation of theAPI to interpret data specifically for combo boxes:

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

Since the delegate only needs to communicate information between themodel and the widget mapper, we only need to provide implementationsof the setEditorData() and setModelData() functions. In manysituations, delegates are responsible for creating and managing editors,but this is unnecessary when they are used with QDataWidgetMapperbecause 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 theeditor is an instance of QComboBox, but instead check for both the absenceof a user property and the presence of a "currentIndex" property.

void Delegate::setEditorData(QWidget *editor,
                       const QModelIndex &amp;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 thecurrently 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 madeavailable 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 &amp;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 databetween the editor and the model.

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

<tbody>
Preferred Properties 

In order to transfer data between models and widgets, eachQDataWidgetMapper needs to know which widget properties to access.Many widgets have a single user property that can be obtained using theuserProperty() function of the relevant meta-object, and this isautomatically used when it is available.</tbody>

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, QDataWidgetMapperin Qt 4.3 will have a new addMapping() overload that accepts a propertyname, making it possible to override the mapper's default behavior andenable widgets without user properties to be used.

</div></div>

[править] Mapping Information from a Database

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

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

id name address typeid
1 Alice 123 Main Street 101
2 Bob PO Box 32 ... 102
3 Carol The Lighthouse ... 103
4 Donald 47338 Park Avenue ... 101
5 Emma Research Station ... 103

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

id description
101 Home
102 Work
103 Other

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

QSqlDatabase db = QSqlDatabase::addDatabase("[[Qt:Документация_4.3.2/qsqlite  | QSQLITE]]");
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 widgetmapper, so we use setTable() to make it the table operated on by themodel.

We need to set up the relation between the two tables. Instead of usinga hard-coded value for the "typeid" column number, we ask the model forits 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 bematched 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 thisrelation, using the previously-stored field index, so that we can use itwith the combo box, just as in the previous example. We use thesetModelColumn() function to ensure that the combo box displays datafrom 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 aQSqlRelationalDelegate to handle the interaction between the relationalmodel and the mapper.

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

[править] Summary

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

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

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