I. L'article original

Qt Quarterly est une revue trimestrielle électronique proposée par Nokia à destination des développeurs et utilisateurs de Qt. Vous pouvez les trouver en version originale.

Nokia, Qt, Qt Quarterly et leurs logos sont des marques déposées de Nokia Corporation en Finlande et/ou dans les autres pays. Les autres marques déposées sont détenues par leurs propriétaires respectifs.

Cet article est la traduction de l'article Mapping Data to Widgets de David Boddie paru dans la Qt Quarterly Issue 21.

Cet article est une traduction de l'un des tutoriels en anglais écrits par Nokia Corporation and/or its subsidiary(-ies), inclus dans la documentation de Qt. Les éventuels problèmes résultant d'une mauvaise traduction ne sont pas imputables à Nokia.

II. Un simple widget de correspondance

La classe QDataWidgetMapper est une classe conçue pour accéder aux données dans un modèle en tableau et afficher les informations obtenues dans une collection de widgets choisis.

Dans sa configuration par défaut, un QDataWidgetMapper a accès à une seule ligne à la fois et associe son contenu à des widgets spécifiques. Pour chaque ligne examinée, il fait correspondre les propriétés utilisateur des widgets à chaque colonne correspondante, comme l'indique le diagramme suivant :

Image non disponible

Voici un petit exemple de code qui crée une simple interface en formulaire, en utilisant un QDataWidgetMapper et un tableau pour mettre à jour le contenu des registres dès que l'utilisateur clique sur un bouton.

Cet exemple est constitué d'une unique classe "window" :

 
Sélectionnez
class Window : public QWidget
{
    Q_OBJECT

public:
    Window(QWidget *parent = 0);

private slots:
    void updateButtons(int row);

private:
    void setupModel();
    ...
    QStandardItemModel *model;
    QDataWidgetMapper *mapper;
};

Elle possède un slot pour conserver une interface cohérente et une fonction privée setupModel() qui crée un modèle contenant quelques données à afficher.

Presque tout est fait dans le constructeur de la classe Window. On commence par initialiser le modèle contenant les données à afficher (on s'y attardera) et assemble l'interface de l'utilisateur.

 
Sélectionnez
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"));

Seuls les widgets éditables seront utilisés avec le QDataWidgetMapper. Les boutons permettent à l'utilisateur de naviguer parmi les registres.

Utiliser le widget mapper en lui-même est assez facile : on construit une instance de QDataWidgetMapper, lui apporte un modèle à utiliser et associe chaque widget à une colonne choisie dans le modèle.

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

On devra s'assurer que le modèle contient les bonnes données dans chaque colonne. Créer un QWidgetMapper est rarement plus compliqué que ça.

Pour rendre l'exemple interactif, on connecte un QPushButton au widget pour que l'utilisateur puisse examiner chaque registre. Pour plus de cohérence, on le connecte au slot "updateButtons()", on peut ainsi choisir d'activer ou non le bouton si nécessaire.

 
Sélectionnez
    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();
}

Une fois les connexions réalisées, on demande au widget de correspondance de se référer à la première ligne du modèle.

Pour montrer l'utilisation la plus simple possible d'un QDataWidgetMapper, on utilise un QStandardItemModel comme table. La fonction setupModel() crée un modèle à taille fixe et l'initialise en utilisant des données préparées à l'aide de trois QStringList :

 
Sélectionnez
void Window::setupModel()
{
    model = new QStandardItemModel(5, 3, this);

    QStringList names;
    names << "Alice" << "Bob" << "Carol"
          << "Donald" << "Emma";

    // On met en place les QStringList "age" et "address" ici.

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

On enregistre les noms, adresses et âges d'un groupe de personnes, respectivement dans les colonnes 0, 1 et 2, chaque ligne contenant les informations relatives à une seule personne. Les colonnes utilisées sont les mêmes que celles spécifiées au widget. Tous les noms seront donc affichés dans un QLineEdit, les adresses dans un QTextEdit et les âges dans un QSpinBox.

Le slot updateButton() est appelé dès que le widget de correspondance lit une ligne du modèle et est simplement utilisé pour activer et désactiver les QPushButton :

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

Quand l'exemple est lancé, les boutons permettent de sélectionner différents registres en changeant la ligne courante dans le modèle. Comme on utilise des widgets éditables, tous les changements effectués aux informations affichées seront répercutés sur le modèle.

III. Utiliser les délégués pour laisser le choix

Pour les widgets ne détenant qu'une seule information, comme un QLineEdit et les autres widgets de l'exemple précédent, utiliser un QDataWigetMapper est un jeu d'enfant. Les problèmes surviennent avec les widgets qui proposent des choix (tels que les QComboBox). On doit alors penser à la façon dont ces choix seront stockés et comment mettre les widgets à jour le cas échéant.

Regardons l'exemple précédent. On utilisera presque la même interface qu'avant, en remplaçant le QSpinBox par un QComboBox. Bien que le combo box puisse afficher la valeur d'une colonne d'un tableau, il n'y a aucun moyen d'enregistrer à la fois le choix courant et les choix possibles d'une manière compréhensible pour un QDataWidgetMapper.

On pourrait mettre un modèle différent sur le combo box. Cependant, ce simple changement le détacherait du widget de correspondance. Pour contrer ce problème, on doit introduire un autre composant fréquent dans l'architecture modèle/vue de Qt : le délégué.

Image non disponible

Le diagramme précédent montre ce que l'on souhaite réaliser : un combo box affichant une liste de choix pour chaque item de la troisième colonne du modèle. À cette fin, on crée un modèle en liste contenant les choix possibles du combo box (« Home », « Work » et « Other »). Dans la troisième colonne du tableau, on fait correspondre ces choix par le numéro de ligne leur correspondant.

En se basant sur l'exemple précédent, on peut utiliser un combo box à la place d'un spin box et la configurer pour utiliser un modèle en liste contenant les choix voulus. On construit ensuite son propre délégué.

La partie intéressante de la classe Window ressemble maintenant à ceci :

 
Sélectionnez
    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);
     ...

Quand on crée le modèle dans la fonction setupModel(), on initialise aussi la variable privée typeModel avec un QStringListModel des choix disponibles dans le combo box.

 
Sélectionnez
void Window::setupModel()
{
    QStringList items;
    items << tr("Home") << tr("Work") << tr("Other");
    typeModel = new QStringListModel(items, this);

    // On met en place le modele ici.
}

Maintenant, le délégué. La classe Delegate dérive de QItemDelegate et apporte une implémentation minimale de l'API qui interprète les données spécialement pour les combo boxes.

 
Sélectionnez
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;
};

Comme les délégués n'ont besoin, pour communiquer, que d'informations entre le modèle et le widget de correspondance, on ne doit implémenter que les fonctions setEditorData() et setModelData(). Dans la plupart des cas, les délégués sont responsables de la création et de l'édition, mais c'est inutile ici, car ils sont utilisés avec un QDataWidgetMapper et l'éditeur existe déjà.

La fonction setEditorData() initialise l'éditeur avec les bonnes données. Pour rendre le code plus générique, on ne vérifie pas explicitement que l'éditeur est une instance de QCombobox, on vérifie l'absence de propriétés d'utilisateur et celle d'une propriété d'index courant, "currentIndex".

 
Sélectionnez
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);
}

On règle la propriété utilisateur en fonction de la valeur fournie par le modèle en changeant l'item visible dans le combo box. On recommence l'implémentation de base pour tous les autres widgets.

La fonction setModelData() est responsable de transférer les données modifiées dans le modèle. Si l'éditeur n'a pas de propriété utilisateur valide, mais possède une propriété "currentIndex", c'est cette valeur qui sera stockée dans le modèle.

 
Sélectionnez
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);
}

Dans tous les autres cas, l'implémentation de base des classes est utilisée pour lier l'éditeur et le modèle.

Dans les deux fonctions, on n'obtient pas les informations affichées dans le combo box, puisqu'elles sont déjà dans un modèle séparé.

III-A. Propriétés utilisateur par défaut

Pour transférer les données entre un modèle et des widgets, chaque QDataWidgetMapper a besoin de savoir à quelle propriété se référer. De nombreux widets n'ont qu'une seule propriété utilisateur (on peut y accéder grâce à la fonction userProperty() de l'objet en question) et est automatiquement choisie quand elle est disponible.

Les problèmes surviennent quand un widget que l'on veut utiliser n'a pas de propriété utilisateur, c'est par exemple le cas avec un QCombobox. Pour résoudre ce problème, QDataWidgetMapper possède une nouvelle fonction, addMapping(), qui prend en argument un nom de propriété. Ainsi, il est possible contourner ces problèmes et d'utiliser les widgets sans propriété utilisateur.

IV. Associer les informations d'après une base de données

Une des plus fréquentes utilisations des QDataWidgetMapper est de connecter une base de données à une vue. Comme la classe elle-même est faite pour fonctionner avec n'importe quel modèle correct, ce devrait être un jeu d'enfant de créer un autre exemple comme le précédent. Pourtant, il s'avère que les applications basées sur des registres requièrent souvent à l'utilisateur de sélectionner parmi un set de choix, rendant ce cas plus intéressant.

Dans la plupart des applications avec une base de données SQL, les deux modèles utilisés dans les exemples précédents seraient représentés à l'aide de deux tables. On créera une table "person" qui contiendra les noms, adresses et une clef étrangère dans une autre table qui contiendra le type d'adresse. La table ressemble à ceci :

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

La valeur de "typeid" correspond à la valeur de la colonne "id" de la table "addresstype", qui ressemble à cela :

id description
101 Home
102 Work
103 Other

Dans la fonction setupModel(), on crée une unique instance de QSqlRelationalTableModel, en utilisant une base de données SQLite en mémoire, remplie avec une série de requêtes SQL (qui ne sont pas montrées ici).

 
Sélectionnez
    QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
    db.setDatabaseName(":memory:");
    
    // On crée ici les deux tables "person" et "addressType"
    
    model = new QSqlRelationalTableModel(this);
    model->setTable("person"); 

La table "person" contient les informations que nous voulons utiliser avec le widget de correspondance. On utilise setTable() avec la table en argument.

On doit créer une relation entre ces deux tables. On demande donc au modèle son champ d'index et l'enregistre pour utilisation future :

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

La relation indique que les valeurs trouvées dans la colonne "typeid" doivent correspondre aux valeurs de la colonne "id" dans la table "adresstype" et que ce sont les valeurs de la colonne « description » qui sont intéressantes.

Dans le constructeur de la classe Window, on obtient le modèle qui gère cette relation. Les valeurs enregistrées précédemment sont utilisées avec le combo box, comme dans l'exemple précédent. On utilise la fonction setModelColumn() pour s'assurer que le combo box affiche bien la donnée de la colonne appropriée :

 
Sélectionnez
    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);

Pour le reste, c'est toujours la même chose qu'auparavant, sauf qu'on utilise un QSqlRelationalDelegate pour gérer les interactions entre le modèle et le widget de correspondance.

Mis à part le SQL, cet exemple est plus ou moins le même qu'avant ; le QSqlRelationalDelegate se comporte différemment de la classe de délégué personnalisée, mais réalise au final la même chose.

V. Récapitulatif

Un QDataWidgetMapper permet d'associer une colonne d'un tableau à un widget spécifique dans une interface de l'utilisateur et d'accéder aux registres un par un.

Les champs qui nécessitent une liste de choix autorisés ne sont pas gérés de la même manière que les autres, on doit leur appliquer un procédé plus compliqué. On utilise les délégués comme intermédiaire entre le modèle et les ressources additionnelles contenant les choix disponibles. On a aussi besoin de widgets comme les QCombobox pour afficher ces choix, plutôt que les données brutes du modèle.

Le module QtSql apporte la classe QSqlRelationalDelegate pour faciliter l'utilisation des modèles SQL, mais on a tout de même besoin de faire attention à créer des relations entre les tables correctement.

Les sources de cet article sont disponibles.

Merci à Thibaut Cuvelier et Claude Leloup pour leur relecture !