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 trouver les versions originales.

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 une traduction de l'article original écrit par Mark Summerfield paru dans la Qt Quarterly Issue 15.

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. Introduction

Imaginons que les courriers électroniques doivent être archivés et proviennent de plusieurs fichiers plats vers une base de données. Durant cette transition, il faudra pouvoir voir les e-mails de différents types d'archives.

Image non disponible
Figure 1 - Liste des courriers électroniques

Créer une vue sous forme de table est simple, comme le montre ce bout de code du constructeur de la fenêtre :

 
Sélectionnez
    view = new QTableView;
    view->setAlternatingRowColors(true);

Pour accéder à une base de données SQL, il convient de paramétrer le pilote de base de données dans le constructeur de la fenêtre :

 
Sélectionnez
    QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");

Quand l'utilisateur clique sur le bouton « Open », une boîte de dialogue d'ouverture de fichier est présentée. L'extension du fichier choisi permet de déterminer quel modèle utiliser avec la vue table.

 
Sélectionnez
    if (fileName.endsWith(".db"))
        setSqlModel(fileName);
    else if (fileName.endsWith(".dat"))
        setFlatFileModel(fileName);

III. Utilisation des modèles SQL

Voici l'implémentation de la méthode setSqlModel() :

 
Sélectionnez
    void MailView::setSqlModel(const QString &fileName)
    {
        QSqlDatabase db = QSqlDatabase::database();
        if (db.isValid())
            db.setDatabaseName(fileName);
        if (!db.isValid() || !db.open()) {
            // Give error message, e.g., using QMessageBox
            return;
        }
    
        QSqlTableModel *model = new QSqlTableModel; // A
        model->setTable("messages");                // B
        model->select();                            // C
    
        model->setHeaderData(0, Qt::Horizontal, tr("ID"));
        model->setHeaderData(1, Qt::Horizontal,
                             tr("Subject"));
        ...
        model->setHeaderData(5, Qt::Horizontal, tr("Body"));
        view->setModel(model);
    }

La classe QSqlTableModel est un modèle de données qui stocke ses données dans une table SQL définie. L'appel de la méthode select() fait effectivement une requête « SELECT * FROM messages ». Il est possible de choisir de paramétrer les noms des colonnes. Si ces noms sont omis, la méthode setHeaderData() récupère le nom des champs dans la base de données et ceux-ci seront utilisés à la place. Lors de l'appel à la méthode setModel(), la vue efface les données existantes dans la vue table et la peuple avec les données du modèle.

L'utilisation de cette première approche donne accès à un certain nombre de fonctionnalités avec, par exemple, la possibilité de trier suivant une colonne particulière en appelant la méthode sortByColumn() de la vue. Une autre approche serait d'appeler setSort() sur le modèle.

Une alternative qui fournit un contrôle plus fin serait de remplacer les lignes commentées A, B et C comme suit :

 
Sélectionnez
    QSqlQueryModel *model = new QSqlQueryModel;
    model->setQuery("SELECT id, subject, sender, recipient, "
                    "date, body FROM messages");

Si cette approche est utilisée, la vue ne pourra pas être triée, mais il est assez facile et potentiellement beaucoup plus souple, d'appeler la méthode model() de la vue pour récupérer le modèle et d'appeler ensuite la méthode setQuery() avec une clause ORDER BY. De façon similaire, les enregistrements peuvent être restreints pour qu'ils soient disponibles dans la vue en utilisant la clause WHERE. Ceci peut être activé dans la classe QSqlTableModel avec setFilter().

IV. Utilisation d'un fichier plat personnalisé

Paramétrer la vue pour utiliser un modèle personnalisé est facile, car les fonctionnalités sont disponibles dans la classe de sous-modèle.

 
Sélectionnez
    void MailView::setFlatFileModel(const QString &fileName)
    {
        view->setModel(new FlatFileModel(fileName));
    }

Il a été choisi de sous-classer la classe FlatFileModel de QAbstractTableModel qui correspond étroitement à la structure des données. L'abstraction des éléments de vue de Qt4 signifie que, pour la vue, la source de données réelles n'est pas pertinente.

Les fichiers .dat sont utilisés pour enregistrer les e-mails en fichier plat avec un encodage Latin-1. Chaque e-mail contient une structure « record » qui enregistre l'identifiant, le sujet, l'expéditeur, le destinataire et les champs de dates, chaque élément étant sur une ligne (par exemple « SUBJECT : This is about », suivi par une ligne vide et 0 ou plusieurs lignes indentées par des tabulations et présentant le contenu du message).

Les vues Qt sont très pratiques pour effectuer des requêtes sur des données à afficher ; ainsi, les plages de données n'occupent pas une grande place en mémoire. Dès qu'un fichier contenant un message est potentiellement grand, on peut choisir de ne pas lire la totalité du fichier en mémoire pour en optimiser l'utilisation avec les classes de vues. Il faudrait plutôt scanner le fichier à la recherche de l'identifiant et sauvegarder la position de cet enregistrement dans le fichier (comme si c'était un fichier binaire). Ensuite, lorsqu'un enregistrement est sélectionné, le fichier est ouvert et parcouru de la position de l'enregistrement précédent et jusqu'à la position de l'enregistrement suivant (ou à la fin du fichier pour le dernier enregistrement). Une fois que les données de l'enregistrement sont chargées en mémoire, celui-ci peut être décomposé en plusieurs parties et affiché dans la vue.

Occasionnellement, le fichier peut être modifié avant l'utilisation (par exemple, un nouveau message peut être ajouté). Le fichier peut être verrouillé, mais cela peut être un inconvénient, s'il a été lu depuis un long moment. Il faudrait donc simplement ajouter la date de dernière modification du fichier et, si elle a changé, rescanner le fichier pour mettre à jour les positions.

Pour les modèles en lecture seule, il suffit de ré-implémenter les méthodes data(), rowCount() et columnCount(), mais l'implémentation de la méthode headerData() est recommandée. Toutes ces fonctions sont constantes, les signatures de ces méthodes seront également constantes. Voici le fichier d'en-tête :

 
Sélectionnez
    class FlatFileModel : public QAbstractTableModel
    {
    public:
        FlatFileModel(const QString &fileName,
                      QObject *parent = 0);
    
        QVariant data(const QModelIndex &index,
                      int role) const;
        QVariant headerData(int section,
                            Qt::Orientation orientation,
                            int role) const;
        int rowCount(const QModelIndex &parent) const;
        int columnCount(const QModelIndex &) const
                { return 6; }
    
    private:
        bool updateRequired() const;
        void updateOffsets() const;
    
        mutable QFile file;
        mutable QDateTime modified;
        mutable QVector<int> offsets;
    };

Le nombre de colonnes est connu (les fichiers de données contiennent six champs), la méthode columnCount() peut donc être implémentée directement dans le fichier d'en-tête. La méthode privée updateRequired() est utilisée pour voir si les positions ont besoin d'être mises à jour, la méthode updateOffsets() effectuant cette mise à jour.

Un seul objet sera utilisé pour la lecture du fichier, ainsi que pour la date et l'heure de modification du fichier et un vecteur contenant les positions. Ces variables doivent être déclarées avec l'option « mutable », car elles seront mises à jour par des méthodes constantes.

Voici maintenant les implémentations en détail :

 
Sélectionnez
    FlatFileModel::FlatFileModel(const QString &fileName,
                                 QObject *parent)
        : QAbstractTableModel(parent)
    {
        file.setFileName(fileName);
        updateOffsets();
    }

Quand un nouveau modèle est construit, le nom du fichier et les positions sont mis à jour.

 
Sélectionnez
    void FlatFileModel::updateOffsets() const
    {
        const int ChunkSize = 50;
        offsets.clear();
        QFileInfo finfo(file);
        qint64 size = finfo.size();
        if (!size || !file.open(QIODevice::ReadOnly))
            return;
        modified = finfo.lastModified();
        offsets.append(0);
        qint64 offset = 0;
        while (size) {
            QByteArray bytes = file.read(ChunkSize);
            if (bytes.isEmpty())
                break;
            size -= bytes.size();
            qint64 i = bytes.indexOf("\nID: ");
            if (i != -1)
                offsets.append(offset + i);
            offset += bytes.size();
        }
        file.close();
        offsets.append(finfo.size());
    }

Le fichier texte doit être traité comme un fichier binaire et lu comme des octets, car on doit traiter des caractères encodés sur huit bits (Latin-1). La variable ChunkSize est initialisée à 50 octets ; elle doit être initialisée avec une valeur plus petite que la taille du plus petit enregistrement du fichier. La première action est d'effacer les positions existantes. Le fichier est ensuite contrôlé : si celui-ci est vide ou ne peut être ouvert, la méthode est terminée immédiatement ; si tout est valide, alors la date de modification du fichier est enregistrée et la position du premier enregistrement ajoutée (0). Ensuite, le fichier est lu par morceaux, en recherchant les champs « ID » et en ajoutant leurs positions dans le vecteur offsets. À la fin du fichier, une position supplémentaire est ajoutée pour la fin du fichier, ce qui sera plus pratique pour la méthode data().

 
Sélectionnez
    QVariant FlatFileModel::data(const QModelIndex &index,
                                 int role) const
    {
        if (updateRequired()) updateOffsets();
    
        if (!index.isValid() || index.row() < 0
                || index.row() >= offsets.size() - 1
                || role != Qt::DisplayRole)
            return QVariant();
    
        if (!file.open(QIODevice::ReadOnly))
            return QVariant();
        qint64 offset = offsets.at(index.row());
        qint64 length = offsets.at(index.row() + 1) - offset;
        file.seek(offset);
        QByteArray bytes = file.read(length);
        file.close();
        if (bytes.size() != length)
            return QVariant();
    
        QString record = QLatin1String(bytes.data());
        QString key;
        switch (index.column()) {
            case 0: key = "ID: "; break;
            case 1: key = "\nSUBJECT: "; break;
            ...
            case 5: key = "\n\n\t"; break;
            default: return QVariant();
        }
        int i = record.indexOf(key);
        if (i != -1) {
            i += key.size();
            if (index.column() != 5) {
                int j = record.indexOf("\n", i);
                if (j != -1)
                    return record.mid(i, j - i + 1);
            } else
                return record.mid(i).replace("\n\t", "\n");
        }
        return QVariant();
    }

La méthode data() est appelée par la vue pour récupérer les données depuis le modèle. Les positions sont mises à jour si nécessaire. Si l'index du modèle passé en paramètre n'est pas valide ou se trouve dans un enregistrement non présent, ou si le rôle ne peut être affiché, une variable de type QVariant vide doit être retournée.

Le nombre d'enregistrements dans la source de données est de offsets.size()-1, car une position supplémentaire a été ajoutée à la fin. Une variable QVariant vide est retournée si le fichier ne peut être ouvert. Ensuite, la position de début de l'enregistrement est récupéré et sa longueur calculée par rapport à la position de l'enregistrement suivant (ou la fin du fichier si le dernier enregistrement est demandé). La méthode seek() est appelée et les données récupérées au besoin, puis converties dans une chaîne QString prête pour l'analyse.

Chaque champ contient un morceau de texte unique qui le précède. Cela permet de choisir le texte à rechercher (la clé), en fonction de chaque index.column() que la vue interroge. La plupart des champs contiennent juste une ligne, on peut donc extraire les données en utilisant la méthode mid() depuis le caractère suivant la clé jusqu'à la nouvelle ligne. Pour le corps du message qui vient à la fin de l'enregistrement, l'extraction débute depuis le début et jusqu'à la fin de l'enregistrement et, dans le même temps, les tabulations qui font partie du format du fichier sont supprimées, mais pas celles faisant partie des données.

 
Sélectionnez
    QVariant FlatFileModel::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("ID");
            case 1: return tr("Subject");
            ...
            case 5: return tr("Body");
            default: return QVariant();
            }
        } else
            return QString("%1").arg(section + 1);
    }

Si la vue récupère les en-têtes de données, qui sont les étiquettes de colonnes ou de lignes, la méthode headerData() doit délivrer le texte approprié. Pour les colonnes, les noms qui doivent être utilisés sont simplement retournés ; pour les lignes, un numéro de ligne est donné par pas d'une unité - la première ligne a le numéro un. La méthode updateRequired() n'a pas besoin d'être appelée, car les en-têtes proviennent du format du fichier.

 
Sélectionnez
int FlatFileModel::rowCount(const QModelIndex &) const
{
   if (updateRequired()) updateOffsets();
        return offsets.size() - 1;
}


Le compteur de ligne est simplement 1 moins le nombre de positions qu'il y a d'enregistrées dans le vecteur, mais il faut être sûr que les positions sont bien à jour.

 
Sélectionnez
bool FlatFileModel::updateRequired() const
{
    return modified != QFileInfo(file).lastModified();
}

Les positions doivent être mises à jour si la date de modification du fichier est plus récente que la date enregistrée.

V. Conclusion

Dans cet article, la vue est utilisée en lecture seule et se base sur un modèle sous-jacent. L'édition des données, l'affichage des clés étrangères ou la personnalisation des vues n'étaient pas le sujet de l'article.

Si l'édition des données est requise, il faudra implémenter des méthodes supplémentaires et utiliser les champs d'édition de Qt ou ré-implémenter les délégués et fournir des champs d'édition propres aux types de données souhaités. Qt 4 est bien plus souple que Qt 3 en ce qui concerne l'édition des données dans les classes SQL.

Si les clés étrangères sont requises, les classes QSqlRelationTableModel peuvent être utilisées, car elles fournissent le support des clés étrangères, par rapport à la super classe QSqlTableModel.

Si les données doivent être présentées en utilisant une vue personnalisée, ceci peut se faire simplement - et ne requiert pas de modifications aux modèles qui sont utilisés pour alimenter la vue personnalisée avec les données.

Les éléments de Qt 4 concernant les vues fournissent un moyen uniforme pour gérer les données dans les applications Qt, quelle que soit la source de données. L'utilisation de la classe de type de données souple QVariant permet de gérer les données de manière flexible et pratique. Les modèles standards et les vues sont fournis et, comme cet article l'a montré, la création de modèle personnalisé est simple.

VI. Remerciements

Au nom de toute l'équipe Qt, j'aimerais adresser le plus grand remerciement à Nokia pour nous avoir autorisés à traduire cet article !

Je tiens à remercier Thibaut Cuvelier pour ses conseils et Jacques Thery pour sa relecture.