Sur cette page

Exemple d'exploration

L'exemple d'exploration montre comment lire des données à partir d'une base de données et valider des modifications à l'aide des classes ` QSqlRelationalTableModel ` et ` QDataWidgetMapper `.

La fenêtre principale affiche des images, que l'utilisateur peut sélectionner, des produits Qt, Qt Quick, Qt Creator et Qt Project.

Lors de l'exécution de l'application d'exemple, un utilisateur peut récupérer des informations sur chaque élément en cliquant sur l'image correspondante. L'application affiche une fenêtre contextuelle contenant les données et permet aux utilisateurs de modifier la description ainsi que l'image. La vue principale est mise à jour lorsque les utilisateurs valident leurs modifications.

L’exemple se compose de trois classes :

  • ImageItem est une classe d’élément graphique personnalisée utilisée pour afficher les images.
  • View est le widget principal de l’application permettant à l’utilisateur de parcourir les différents éléments.
  • InformationWindow affiche les informations demandées, permettant aux utilisateurs de les modifier et de valider leurs modifications dans la base de données.

Nous allons d’abord examiner la classe ` InformationWindow ` pour voir comment lire et modifier des données dans une base de données. Ensuite, nous passerons en revue le widget principal de l’application, c’est-à-dire la classe ` View `, ainsi que la classe associée ` ImageItem `.

Définition de la classe InformationWindow

La classe ` InformationWindow ` est un widget personnalisé héritant de ` QWidget` :

class InformationWindow : public QDialog
{
    Q_OBJECT
public:
    InformationWindow(int id, QSqlRelationalTableModel *items,
                      QWidget *parent = nullptr);
    int id() const;

Q_SIGNALS:
    void imageChanged(int id, const QString &fileName);

Lorsque nous créons une fenêtre d’information, nous transmettons au constructeur l’ID de l’élément associé, un pointeur vers le modèle et un parent. Nous utiliserons le pointeur vers le modèle pour remplir notre fenêtre de données, tout en transmettant le paramètre parent à la classe de base. L’ID est stocké pour référence ultérieure.

Une fois la fenêtre créée, nous utiliserons la fonction publique ` id() ` pour la localiser chaque fois qu’une information concernant l’emplacement donné est demandée. Nous utiliserons également l’ID pour mettre à jour le widget principal de l’application lorsque les utilisateurs valideront leurs modifications dans la base de données ; c’est-à-dire que nous émettrons un signal contenant l’ID et le nom du fichier en tant que paramètres chaque fois que les utilisateurs modifieront l’image associée.

private Q_SLOTS:
    void revert();
    void submit();
    void enableButtons(bool enable);

Comme nous permettons aux utilisateurs de modifier certaines données, nous devons fournir des fonctionnalités pour annuler et valider leurs modifications. Le slot ` enableButtons() ` est fourni à des fins de commodité pour activer et désactiver les différents boutons lorsque cela est nécessaire.

private:
    void createButtons();

    int itemId;
    QString displayedImage;

    QComboBox *imageFileEditor = nullptr;
    QLabel *itemText = nullptr;
    QTextEdit *descriptionEditor = nullptr;

    QPushButton *closeButton = nullptr;
    QPushButton *submitButton = nullptr;
    QPushButton *revertButton = nullptr;
    QDialogButtonBox *buttonBox = nullptr;

    QDataWidgetMapper *mapper = nullptr;
};

La fonction ` createButtons() ` est également une fonction de commodité, fournie pour simplifier le constructeur. Comme mentionné ci-dessus, nous stockons l’ID de l’élément pour référence ultérieure. Nous stockons également le nom du fichier image actuellement affiché afin de pouvoir déterminer quand émettre le signal ` imageChanged() `.

La fenêtre d’informations utilise la classe ` QLabel ` pour afficher le nom d’un élément. Le fichier image associé est affiché à l’aide d’une instance de ` QComboBox `, tandis que la description est affichée à l’aide de ` QTextEdit`. De plus, la fenêtre comporte trois boutons permettant de contrôler le flux de données et l’affichage ou non de la fenêtre.

Enfin, nous déclarons un mappeur. La classe ` QDataWidgetMapper ` assure le mappage entre une section d’un modèle de données et des widgets. Nous utiliserons ce mappeur pour extraire des données de la base de données fournie, en mettant à jour cette dernière chaque fois que l’utilisateur modifie les données.

Implémentation de la classe `InformationWindow`

Le constructeur prend trois arguments : un identifiant d’élément, un pointeur vers la base de données et un widget parent. Le pointeur vers la base de données est en réalité un pointeur vers un objet ` QSqlRelationalTableModel ` fournissant un modèle de données modifiable (avec prise en charge des clés étrangères) pour notre table de base de données.

InformationWindow::InformationWindow(int id, QSqlRelationalTableModel *items,
                                     QWidget *parent)
    : QDialog(parent)
{
    QLabel *itemLabel = new QLabel(tr("Item:"));
    QLabel *descriptionLabel = new QLabel(tr("Description:"));
    QLabel *imageFileLabel = new QLabel(tr("Image file:"));

    createButtons();

    itemText = new QLabel;
    descriptionEditor = new QTextEdit;

Nous commençons par créer les différents widgets nécessaires à l’affichage des données contenues dans la base de données. La plupart des widgets sont créés de manière simple. Mais notez la liste déroulante affichant le nom du fichier image :

    imageFileEditor = new QComboBox;
    imageFileEditor->setModel(items->relationModel(1));
    imageFileEditor->setModelColumn(items->relationModel(1)->fieldIndex("file"));

Dans cet exemple, les informations relatives aux éléments sont stockées dans une table de base de données nommée « items ». Lors de la création du modèle, nous utiliserons une clé étrangère pour établir une relation entre cette table et une deuxième table de base de données, « images », contenant les noms des fichiers image disponibles. Nous reviendrons sur la manière de procéder lors de l’étude de la classe View. La raison d’être d’une telle relation est toutefois de garantir que l’utilisateur ne puisse choisir que parmi des fichiers image prédéfinis.

Le modèle correspondant à la table de base de données « images » est accessible via la fonction relationModel() de la classe QSqlRelationalTableModel, qui requiert la clé étrangère (dans ce cas, le numéro de la colonne « imagefile ») comme argument. Nous utilisons la fonction setModel() de QComboBox pour que la liste déroulante utilise le modèle « images ». Et, comme ce modèle comporte deux colonnes (« itemid » et « file »), nous spécifions également quelle colonne nous souhaitons rendre visible à l’aide de la fonction QComboBox::setModelColumn().

    mapper = new QDataWidgetMapper(this);
    mapper->setModel(items);
    mapper->setSubmitPolicy(QDataWidgetMapper::ManualSubmit);
    mapper->setItemDelegate(new QSqlRelationalDelegate(mapper));
    mapper->addMapping(imageFileEditor, 1);
    mapper->addMapping(itemText, 2, "text");
    mapper->addMapping(descriptionEditor, 3);
    mapper->setCurrentIndex(id);

Nous créons ensuite le mappeur. La classe QDataWidgetMapper nous permet de créer des widgets sensibles aux données en les mappant à des sections d’un modèle d’élément.

La fonction addMapping() ajoute un mappage entre le widget donné et la section spécifiée du modèle. Si l’orientation du mappeur est horizontale (par défaut), la section correspond à une colonne du modèle ; dans le cas contraire, il s’agit d’une ligne. Nous appelons la fonction ` setCurrentIndex()` pour initialiser les widgets avec les données associées à l’ID d’élément donné. À chaque changement d’index courant, tous les widgets sont mis à jour avec le contenu du modèle.

Nous définissons également la politique de soumission du mappeur sur ` QDataWidgetMapper::ManualSubmit`. Cela signifie qu’aucune donnée n’est soumise à la base de données tant que l’utilisateur n’a pas explicitement demandé une soumission (l’alternative est ` QDataWidgetMapper::AutoSubmit`, qui soumet automatiquement les modifications lorsque le widget correspondant perd le focus). Enfin, nous spécifions le délégué « item » que la vue du mappeur doit utiliser pour ses éléments. La classe ` QSqlRelationalDelegate ` représente un délégué qui, contrairement au délégué par défaut, active la fonctionnalité de liste déroulante pour les champs qui sont des clés étrangères vers d’autres tables (comme « imagefile » dans notre table « items »).

    connect(descriptionEditor, &QTextEdit::textChanged, this, [this]() { enableButtons(true); });
    connect(imageFileEditor, &QComboBox::currentIndexChanged, this, [this]() { enableButtons(true); });

    QFormLayout *formLayout = new QFormLayout;
    formLayout->addRow(itemLabel, itemText);
    formLayout->addRow(imageFileLabel, imageFileEditor);
    formLayout->addRow(descriptionLabel, descriptionEditor);

    QVBoxLayout *layout = new QVBoxLayout;
    layout->addLayout(formLayout);
    layout->addWidget(buttonBox);
    setLayout(layout);

    itemId = id;
    displayedImage = imageFileEditor->currentText();

    setWindowFlags(Qt::Window);
    enableButtons(false);
    setWindowTitle(itemText->text());
}

Enfin, nous connectons les signaux « something’s changed » des éditeurs à notre slot personnalisé « enableButtons », permettant ainsi aux utilisateurs de valider ou d’annuler leurs modifications. Nous devons utiliser des lambdas pour connecter le slot « enableButtons » car sa signature ne correspond pas à celle de « QTextEdit::textChanged » et « QComboBox::currentIndexChanged ».

Nous ajoutons tous les widgets dans une mise en page, enregistrons l’ID de l’élément et le nom du fichier image affiché pour référence ultérieure, puis définissons le titre de la fenêtre et sa taille initiale.

Notez que nous définissons également l’indicateur de fenêtre « Qt::Window » pour indiquer que notre widget est en réalité une fenêtre, dotée d’un cadre de système de fenêtrage et d’une barre de titre.

int InformationWindow::id() const
{
    return itemId;
}

Lorsqu’une fenêtre est créée, elle n’est pas supprimée tant que l’application principale n’est pas fermée (c’est-à-dire que si l’utilisateur ferme la fenêtre d’information, celle-ci est simplement masquée). C’est pourquoi nous ne souhaitons pas créer plus d’un objet ` InformationWindow ` par élément, et nous fournissons la fonction publique ` id() ` afin de pouvoir déterminer si une fenêtre existe déjà pour un emplacement donné lorsque l’utilisateur demande des informations à son sujet.

void InformationWindow::revert()
{
    mapper->revert();
    enableButtons(false);
}

Le slot ` revert() ` est déclenché chaque fois que l’utilisateur clique sur le bouton ` Revert `.

Comme nous avons défini la politique de validation « QDataWidgetMapper::ManualSubmit », aucune des modifications apportées par l’utilisateur n’est répercutée dans le modèle, à moins que l’utilisateur ne choisisse explicitement de toutes les valider. Néanmoins, nous pouvons utiliser le slot « revert() » de « QDataWidgetMapper » pour réinitialiser les widgets de l’éditeur, en les remplissant à nouveau avec les données actuelles du modèle.

void InformationWindow::submit()
{
    QString newImage(imageFileEditor->currentText());

    if (displayedImage != newImage) {
        displayedImage = newImage;
        emit imageChanged(itemId, newImage);
    }

    mapper->submit();
    mapper->setCurrentIndex(itemId);

    enableButtons(false);
}

De même, le slot submit() est déclenché chaque fois que les utilisateurs décident de valider leurs modifications en cliquant sur le bouton Submit.

Nous utilisons le slot submit() de QDataWidgetMapper pour envoyer toutes les modifications des widgets mappés vers le modèle, c'est-à-dire vers la base de données. Pour chaque section mappée, le délégué d'élément lit alors la valeur actuelle du widget et la définit dans le modèle. Enfin, la fonction ` submit()` du modèle est appelée pour indiquer au modèle qu’il doit envoyer tout ce qu’il a mis en cache vers le stockage permanent.

Notez qu’avant toute soumission de données, nous vérifions si l’utilisateur a choisi un autre fichier image en utilisant la variable ` displayedImage ` précédemment stockée comme référence. Si les noms de fichier actuel et stocké diffèrent, nous stockons le nouveau nom de fichier et émettons le signal ` imageChanged() `.

void InformationWindow::createButtons()
{
    closeButton = new QPushButton(tr("&Close"));
    revertButton = new QPushButton(tr("&Revert"));
    submitButton = new QPushButton(tr("&Submit"));

    closeButton->setDefault(true);

    connect(closeButton, &QPushButton::clicked, this, &InformationWindow::close);
    connect(revertButton, &QPushButton::clicked, this, &InformationWindow::revert);
    connect(submitButton, &QPushButton::clicked, this, &InformationWindow::submit);

La fonction ` createButtons() ` est fournie à des fins de commodité, c’est-à-dire pour simplifier le constructeur.

Nous définissons le bouton « Close » comme bouton par défaut, c’est-à-dire le bouton sur lequel l’utilisateur clique lorsqu’il appuie sur « Enter », et nous connectons son signal « clicked() » au slot « close() » du widget. Comme mentionné plus haut, la fermeture de la fenêtre ne fait que masquer le widget ; celui-ci n’est pas supprimé. Nous connectons également les boutons « Submit » et « Revert » aux slots correspondants « submit() » et « revert() ».

    buttonBox = new QDialogButtonBox(this);
    buttonBox->addButton(submitButton, QDialogButtonBox::AcceptRole);
    buttonBox->addButton(revertButton, QDialogButtonBox::ResetRole);
    buttonBox->addButton(closeButton, QDialogButtonBox::RejectRole);
}

La classe ` QDialogButtonBox ` est un widget qui présente des boutons selon une disposition adaptée au style actuel du widget. Les boîtes de dialogue, comme notre fenêtre d’information, présentent généralement les boutons selon une disposition conforme aux directives d’interface de la plateforme concernée. Invariablement, les différentes plateformes ont des dispositions différentes pour leurs boîtes de dialogue. ` QDialogButtonBox ` nous permet d’ajouter des boutons en utilisant automatiquement la disposition adaptée à l’environnement de bureau de l’utilisateur.

La plupart des boutons d’une boîte de dialogue remplissent des rôles spécifiques. Nous attribuons aux boutons « Submit » et « Revert » le rôle « reset », c’est-à-dire que le fait d’appuyer sur le bouton réinitialise les champs aux valeurs par défaut (dans notre cas, les informations contenues dans la base de données). Le rôle « reject » indique que cliquer sur le bouton entraîne le rejet de la boîte de dialogue. En revanche, comme nous ne faisons que masquer la fenêtre d’informations, toutes les modifications apportées par l’utilisateur seront conservées jusqu’à ce que celui-ci les annule ou les valide explicitement.

void InformationWindow::enableButtons(bool enable)
{
    revertButton->setEnabled(enable);
    submitButton->setEnabled(enable);
}

Le slot « enableButtons() » est appelé pour activer les boutons chaque fois que l’utilisateur modifie les données affichées. De même, lorsque l’utilisateur choisit de valider les modifications, les boutons sont désactivés pour indiquer que les données actuelles sont enregistrées dans la base de données.

Cela conclut la classe ` InformationWindow `. Voyons maintenant comment nous l’avons utilisée dans notre application d’exemple.

Définition de la classe View

La classe ` View ` représente la fenêtre principale de l’application et hérite de ` QGraphicsView` :

class View : public QGraphicsView
{
    Q_OBJECT
public:
    View(const QString &items, const QString &images, QWidget *parent = nullptr);

protected:
    void mouseReleaseEvent(QMouseEvent *event) override;

private Q_SLOTS:
    void updateImage(int id, const QString &fileName);

La classe ` QGraphicsView ` fait partie du Graphics View Framework que nous utiliserons pour afficher les images. Afin de pouvoir répondre aux interactions de l’utilisateur en affichant la fenêtre d’informations appropriée lorsque l’on clique sur l’image, nous réimplémentons la fonction ` mouseReleaseEvent()` de ` QGraphicsView`.

Notez que le constructeur attend les noms de deux tables de base de données : l’une contenant les informations détaillées sur les éléments, et l’autre contenant les noms des fichiers image disponibles. Nous fournissons également un slot privé updateImage() pour intercepter le signal imageChanged() de InformationWindow, qui est émis chaque fois que l’utilisateur modifie une image associée à l’élément.

private:
    void addItems();
    InformationWindow *findWindow(int id) const;
    void showInformation(ImageItem *image);

    QGraphicsScene *scene;
    QList<InformationWindow *> informationWindows;

La fonction ` addItems() ` est une fonction de commodité fournie pour simplifier le constructeur. Elle n’est appelée qu’une seule fois, pour créer les différents éléments et les ajouter à la vue.

La fonction ` findWindow() `, en revanche, est fréquemment utilisée. Elle est appelée depuis la fonction ` showInformation() ` pour déterminer si une fenêtre a déjà été créée pour l’élément donné (chaque fois que nous créons un objet ` InformationWindow `, nous stockons une référence à celui-ci dans la liste ` informationWindows `). Cette dernière fonction est à son tour appelée depuis notre implémentation personnalisée de ` mouseReleaseEvent() `.

    QSqlRelationalTableModel *itemTable;
};

Enfin, nous déclarons un pointeur de type ` QSqlRelationalTableModel `. Comme mentionné précédemment, la classe ` QSqlRelationalTableModel ` fournit un modèle de données modifiable prenant en charge les clés étrangères. Il y a quelques points à garder à l’esprit lors de l’utilisation de la classe ` QSqlRelationalTableModel ` : la table doit disposer d’une clé primaire déclarée et cette clé ne peut pas contenir de référence à une autre table, c’est-à-dire qu’elle ne peut pas être une clé étrangère. Notez également que si une table relationnelle contient des clés faisant référence à des lignes inexistantes dans la table référencée, les lignes contenant ces clés invalides ne seront pas exposées via le modèle. Il incombe à l’utilisateur ou à la base de données de maintenir l’intégrité référentielle.

Implémentation de la classe `View`

Bien que le constructeur demande les noms de la table contenant les détails du bureau ainsi que ceux de la table contenant les noms des fichiers image disponibles, il suffit de créer un objet ` QSqlRelationalTableModel ` pour la table « items » :

View::View(const QString &items, const QString &images, QWidget *parent)
    : QGraphicsView(parent)
{
    itemTable = new QSqlRelationalTableModel(this);
    itemTable->setTable(items);
    itemTable->setRelation(1, QSqlRelation(images, "itemid", "file"));
    itemTable->select();

En effet, une fois que nous disposons d’un modèle contenant les détails des articles, nous pouvons créer une relation avec les fichiers image disponibles à l’aide de la fonction ` setRelation()` de ` QSqlRelationalTableModel`. Cette fonction crée une clé étrangère pour la colonne du modèle indiquée. La clé est spécifiée par l’objet ` QSqlRelation ` fourni, construit à partir du nom de la table à laquelle la clé fait référence, du champ auquel la clé est mappée et du champ qui doit être présenté à l’utilisateur.

Notez que la définition de la table spécifie uniquement sur quelle table le modèle opère ; en d’autres termes, nous devons appeler explicitement la fonction ` select()` du modèle pour remplir notre modèle.

    scene = new QGraphicsScene(this);
    scene->setSceneRect(0, 0, 465, 365);
    setScene(scene);

    addItems();

    setMinimumSize(470, 370);
    setMaximumSize(470, 370);

    QLinearGradient gradient(QPointF(0, 0), QPointF(0, 370));
    gradient.setColorAt(0, QColor("#868482"));
    gradient.setColorAt(1, QColor("#5d5b59"));
    setBackgroundBrush(gradient);
}

Ensuite, nous créons le contenu de notre vue, c’est-à-dire la scène et ses éléments. Les étiquettes sont des objets QGraphicsTextItem classiques, tandis que les images sont des instances de la classe ImageItem, dérivée de QGraphicsPixmapItem. Nous y reviendrons brièvement lors de l’examen de la fonction addItems().

Enfin, nous définissons les contraintes de taille et le titre de la fenêtre du widget principal de l’application.

void View::addItems()
{
    int itemCount = itemTable->rowCount();

    int imageOffset = 150;
    int leftMargin = 70;
    int topMargin = 40;

    for (int i = 0; i < itemCount; i++) {
        QSqlRecord record = itemTable->record(i);

        int id = record.value("id").toInt();
        QString file = record.value("file").toString();
        QString item = record.value("itemtype").toString();

        int columnOffset = ((i % 2) * 37);
        int x = ((i % 2) * imageOffset) + leftMargin + columnOffset;
        int y = ((i / 2) * imageOffset) + topMargin;

        ImageItem *image = new ImageItem(id, QPixmap(":/" + file));
        image->setData(0, i);
        image->setPos(x, y);
        scene->addItem(image);

        QGraphicsTextItem *label = scene->addText(item);
        label->setDefaultTextColor(QColor("#d7d6d5"));
        QPointF labelOffset((120 - label->boundingRect().width()) / 2, 120.0);
        label->setPos(QPointF(x, y) + labelOffset);
    }
}

La fonction ` addItems() ` n’est appelée qu’une seule fois lors de la création de la fenêtre principale de l’application. Pour chaque ligne de la table de la base de données, nous extrayons d’abord l’enregistrement correspondant à l’aide de la fonction ` record()` du modèle. La classe ` QSqlRecord ` encapsule à la fois les fonctionnalités et les caractéristiques d’un enregistrement de base de données, et prend en charge l’ajout et la suppression de champs ainsi que la définition et la récupération des valeurs de ces champs. La fonction ` QSqlRecord::value()` renvoie la valeur du champ dont le nom ou l’index est spécifié sous la forme d’un objet ` QVariant `.

Pour chaque enregistrement, nous créons un élément « label » ainsi qu’un élément « image », calculons leur position et les ajoutons à la scène. Les éléments « image » sont représentés par des instances de la classe ` ImageItem `. La raison pour laquelle nous devons créer une classe d’élément personnalisée est que nous souhaitons intercepter les événements de survol de l’élément, afin d’animer celui-ci lorsque le curseur de la souris survole l’image (par défaut, aucun élément n’accepte les événements de survol). Veuillez consulter la documentation du Graphics View Framework et les exemples de Graphics View pour plus de détails.

void View::mouseReleaseEvent(QMouseEvent *event)
{
    if (QGraphicsItem *item = itemAt(event->position().toPoint())) {
        if (ImageItem *image = qgraphicsitem_cast<ImageItem *>(item))
            showInformation(image);
    }
    QGraphicsView::mouseReleaseEvent(event);
}

Nous réimplémentons le gestionnaire d’événements ` mouseReleaseEvent()` de ` QGraphicsView` pour répondre aux interactions de l’utilisateur. Si l’utilisateur clique sur l’un des éléments d’image, cette fonction appelle la fonction privée ` showInformation() ` afin d’afficher la fenêtre d’informations associée.

Le Graphics View Framework fournit la fonction ` qgraphicsitem_cast()` pour déterminer si l’instance ` QGraphicsItem ` donnée est d’un type donné. Notez que si l’événement n’est lié à aucun de nos éléments d’image, nous le transmettons à l’implémentation de la classe de base.

void View::showInformation(ImageItem *image)
{
    int id = image->id();
    if (id < 0 || id >= itemTable->rowCount())
        return;

    InformationWindow *window = findWindow(id);
    if (!window) {
        window = new InformationWindow(id, itemTable, this);

        connect(window, &InformationWindow::imageChanged,
                this, &View::updateImage);

        window->move(pos() + QPoint(20, 40));
        window->show();
        informationWindows.append(window);
    }

    if (window->isVisible()) {
        window->raise();
        window->activateWindow();
    } else
        window->show();
}

La fonction ` showInformation() ` reçoit un objet ` ImageItem ` en argument et commence par extraire l’ID de l’élément.

Elle détermine ensuite s’il existe déjà une fenêtre d’ imageChanged() s pour cet emplacement. Si aucune fenêtre n’existe pour l’emplacement donné, nous en créons une en transmettant l’ID de l’élément, un pointeur vers le modèle et notre vue en tant que parent au constructeur de ` InformationWindow `. Notez que nous connectons le signal « » de la fenêtre d’information au slot « updateImage() » de ce widget, avant de lui attribuer une position appropriée et de l’ajouter à la liste des fenêtres existantes. S’il existe déjà une fenêtre pour l’emplacement donné et que cette fenêtre est visible, cela garantit que la fenêtre est placée au premier plan de la pile de widgets et activée. Si elle est masquée, l'appel de son slot ` show()` donne le même résultat.

void View::updateImage(int id, const QString &fileName)
{
    QList<QGraphicsItem *> items = scene->items();

    while(!items.empty()) {
        QGraphicsItem *item = items.takeFirst();

        if (ImageItem *image = qgraphicsitem_cast<ImageItem *>(item)) {
            if (image->id() == id){
                image->setPixmap(QPixmap(":/" +fileName));
                image->adjust();
                break;
            }
        }
    }
}

Le slot ` updateImage() ` prend comme arguments un identifiant d’élément et le nom d’un fichier image. Il filtre les éléments d’image et met à jour celui qui correspond à l’identifiant d’élément donné, en utilisant le fichier image fourni.

InformationWindow *View::findWindow(int id) const
{
    for (auto window : informationWindows) {
        if (window && (window->id() == id))
            return window;
    }
    return nullptr;
}

La fonction ` findWindow() ` parcourt simplement la liste des fenêtres existantes et renvoie un pointeur vers la fenêtre correspondant à l’ID d’élément donné, ou ` nullptr ` si la fenêtre n’existe pas.

Enfin, jetons un coup d’œil à notre classe personnalisée « ImageItem » :

Définition de la classe `ImageItem`

La classe ` ImageItem ` est fournie pour faciliter l’animation des éléments d’image. Elle hérite de ` QGraphicsPixmapItem ` et réimplémente ses gestionnaires d’événements de survol :

class ImageItem : public QObject, public QGraphicsPixmapItem
{
    Q_OBJECT
public:
    enum { Type = UserType + 1 };

    ImageItem(int id, const QPixmap &pixmap, QGraphicsItem *parent = nullptr);

    int type() const override { return Type; }
    void adjust();
    int id() const;

protected:
    void hoverEnterEvent(QGraphicsSceneHoverEvent *event) override;
    void hoverLeaveEvent(QGraphicsSceneHoverEvent *event) override;

private Q_SLOTS:
    void setFrame(int frame);
    void updateItemPosition();

private:
    QTimeLine timeLine;
    int recordId;
    double z;
};

Nous déclarons une valeur d’énumération Type pour notre élément personnalisé et réimplémentons la méthode type(). Cela nous permet d’utiliser en toute sécurité la méthode qgraphicsitem_cast(). De plus, nous implémentons une fonction publique id() afin de pouvoir identifier l’emplacement associé, ainsi qu’une fonction publique adjust() qui peut être appelée pour garantir que l’élément image se voit attribuer la taille souhaitée, quel que soit le fichier image d’origine.

L’animation est implémentée à l’aide de la classe QTimeLine, en combinaison avec les gestionnaires d’événements et le slot privé setFrame(): l’élément image s’agrandit lorsque le curseur de la souris survole celui-ci, puis revient à sa taille d’origine lorsque le curseur quitte ses limites.

Enfin, nous stockons l’ID de l’élément auquel cet enregistrement particulier est associé, ainsi qu’une valeur z. Dans le Graphics View Framework, la valeur z d’un élément détermine sa position dans la pile des éléments. Un élément ayant une valeur z élevée sera affiché par-dessus un élément ayant une valeur z inférieure s’ils partagent le même élément parent. Nous fournissons également une fonction « updateItemPosition() » pour actualiser la vue lorsque cela est nécessaire.

Implémentation de la classe `ImageItem`

La classe ` ImageItem ` n’est en réalité qu’une extension de ` QGraphicsPixmapItem ` dotée de quelques fonctionnalités supplémentaires ; en effet, nous pouvons transmettre la plupart des arguments du constructeur (la carte de pixels, le parent et la scène) au constructeur de la classe de base :

ImageItem::ImageItem(int id, const QPixmap &pixmap, QGraphicsItem *parent)
    : QGraphicsPixmapItem(pixmap, parent)
{
    recordId = id;
    setAcceptHoverEvents(true);

    timeLine.setDuration(150);
    timeLine.setFrameRange(0, 150);

    connect(&timeLine, &QTimeLine::frameChanged, this, &ImageItem::setFrame);
    connect(&timeLine, &QTimeLine::finished, this, &ImageItem::updateItemPosition);

    adjust();
}

Nous stockons ensuite l’ID pour référence ultérieure et nous nous assurons que notre élément image acceptera les événements de survol. Les événements de survol sont déclenchés lorsqu’aucun élément n’est actuellement sélectionné par la souris. Ils sont envoyés lorsque le curseur de la souris entre dans un élément, lorsqu’il se déplace à l’intérieur de celui-ci et lorsqu’il quitte l’élément. Comme nous l’avons mentionné précédemment, aucun des éléments du Graphics View Framework n’accepte les événements de survol par défaut.

La classe ` QTimeLine ` fournit une timeline permettant de contrôler les animations. Sa propriété ` duration ` contient la durée totale de la timeline en millisecondes. Par défaut, la timeline s’exécute une seule fois, du début à la fin. La fonction ` QTimeLine::setFrameRange()` définit le compteur d’images de la timeline ; lorsque la timeline est en cours d’exécution, le signal ` frameChanged()` est émis à chaque changement d’image. Nous définissons la durée et la plage d’images de notre animation, puis nous connectons les signaux ` frameChanged()` et ` finished()` de la timeline à nos slots privés ` setFrame() ` et ` updateItemPosition() `.

Enfin, nous appelons ` adjust() ` pour nous assurer que l’élément prend la taille souhaitée.

void ImageItem::hoverEnterEvent(QGraphicsSceneHoverEvent * /*event*/)
{
    timeLine.setDirection(QTimeLine::Forward);

    if (z != 1.0) {
        z = 1.0;
        updateItemPosition();
    }

    if (timeLine.state() == QTimeLine::NotRunning)
        timeLine.start();
}

void ImageItem::hoverLeaveEvent(QGraphicsSceneHoverEvent * /*event*/)
{
    timeLine.setDirection(QTimeLine::Backward);
    if (z != 0.0)
        z = 0.0;

    if (timeLine.state() == QTimeLine::NotRunning)
        timeLine.start();
}

Chaque fois que le curseur de la souris entre ou sort de l’élément image, les gestionnaires d’événements correspondants sont déclenchés : nous définissons d’abord le sens de la ligne temporelle, ce qui fait respectivement s’agrandir ou se réduire l’élément. Ensuite, nous modifions la valeur z de l’élément si elle n’est pas déjà définie sur la valeur attendue.

Dans le cas des événements de survol (hover enter ), nous mettons immédiatement à jour la position de l’élément, car nous voulons qu’il apparaisse au-dessus de tous les autres éléments dès qu’il commence à s’agrandir. Dans le cas des événements de sortie de survol (hover leave ), en revanche, nous reportons la mise à jour effective pour obtenir le même résultat. Mais n’oubliez pas que, lors de la création de notre élément, nous avons connecté le signal ` finished()` de la timeline au slot ` updateItemPosition() `. De cette manière, l’élément se voit attribuer la position correcte dans la pile d’éléments une fois l’animation terminée. Enfin, si la timeline n’est pas déjà en cours d’exécution, nous la lançons.

void ImageItem::setFrame(int frame)
{
    adjust();
    QPointF center = boundingRect().center();

    setTransform(QTransform::fromTranslate(center.x(), center.y()), true);
    setTransform(QTransform::fromScale(1 + frame / 300.0, 1 + frame / 300.0), true);
    setTransform(QTransform::fromTranslate(-center.x(), -center.y()), true);
}

Lorsque la timeline est en cours d’exécution, elle déclenche le slot ` setFrame() ` à chaque changement d’image en raison de la connexion que nous avons établie dans le constructeur de l’élément. C’est ce slot qui contrôle l’animation, en agrandissant ou en réduisant l’élément image étape par étape.

Nous appelons d’abord la fonction ` adjust() ` pour nous assurer de partir de la taille d’origine de l’élément. Ensuite, nous redimensionnons l’élément avec un facteur dépendant de la progression de l’animation (à l’aide du paramètre ` frame `). Notez que, par défaut, la transformation s’effectue par rapport au coin supérieur gauche de l’élément. Comme nous souhaitons que l’élément soit transformé par rapport à son centre, nous devons translaté le système de coordonnées avant de redimensionner l’élément.

Au final, seules les fonctions pratiques suivantes subsistent :

void ImageItem::adjust()
{
    setTransform(QTransform::fromScale(120.0 / boundingRect().width(),
                                       120.0 / boundingRect().height()));
}

int ImageItem::id() const
{
    return recordId;
}

void ImageItem::updateItemPosition()
{
    setZValue(z);
}

La fonction ` adjust() ` définit et applique une matrice de transformation, garantissant ainsi que notre élément d'image s'affiche à la taille souhaitée, quelle que soit la taille de l'image source. La fonction ` id() ` est triviale et sert simplement à identifier l'élément. Dans le slot ` updateItemPosition() `, nous appelons la fonction ` QGraphicsItem::setZValue()` pour définir l'élévation de l'élément.

Exemple de projet @ code.qt.io

© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.