Sur cette page

Exemple de scène de diagramme

Démonstration de l'utilisation du cadre de travail Graphics View.

Application avec divers graphiques et icônes d'organigramme

L'exemple de scène de diagramme est une application dans laquelle vous pouvez créer un diagramme d'organigramme. Il est possible d'ajouter des formes d'organigramme et du texte et de relier les formes par des flèches, comme le montre l'image ci-dessus. Les formes, les flèches et le texte peuvent être colorés différemment et il est possible de modifier la police, le style et le soulignement du texte.

Le cadre de vues graphiques Qt est conçu pour gérer et afficher des éléments graphiques 2D personnalisés. Les principales classes du cadre sont QGraphicsItem, QGraphicsScene et QGraphicsView. La scène graphique gère les éléments et leur fournit une surface. QGraphicsView est un widget utilisé pour rendre une scène à l'écran. Voir le Graphics View Framework pour une description plus détaillée du cadre.

Dans cet exemple, nous montrons comment créer des scènes graphiques et des éléments personnalisés en mettant en œuvre des classes qui héritent de QGraphicsScene et QGraphicsItem.

En particulier, nous montrons comment

  • Créer des éléments graphiques personnalisés.
  • Gérer les événements de la souris et le mouvement des éléments.
  • Implémenter une scène graphique qui peut gérer nos éléments personnalisés.
  • Peindre des éléments personnalisés.
  • Créer un élément de texte mobile et modifiable.

L'exemple se compose des classes suivantes :

  • MainWindow crée les widgets et les affiche dans une page QMainWindow. Elle gère également l'interaction entre les widgets et la scène graphique, la vue et les éléments.
  • DiagramItem hérite de QGraphicsPolygonItem et représente une forme d'organigramme.
  • TextDiagramItem hérite de QGraphicsTextItem et représente les éléments de texte dans le diagramme. La classe permet de déplacer l'élément à l'aide de la souris, ce qui n'est pas le cas dans la classe QGraphicsTextItem.
  • Arrow hérite de QGraphicsLineItem et représente une flèche qui relie deux DiagramItems.
  • DiagramScene hérite de QGraphicsDiagramScene et prend en charge DiagramItem, Arrow et DiagramTextItem (en plus de la prise en charge déjà assurée par QGraphicsScene).

Définition de la classe MainWindow

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
   MainWindow();

private slots:
    void backgroundButtonGroupClicked(QAbstractButton *button);
    void buttonGroupClicked(QAbstractButton *button);
    void deleteItem();
    void pointerGroupClicked();
    void bringToFront();
    void sendToBack();
    void itemInserted(DiagramItem *item);
    void textInserted(QGraphicsTextItem *item);
    void currentFontChanged(const QFont &font);
    void fontSizeChanged(const QString &size);
    void sceneScaleChanged(const QString &scale);
    void textColorChanged();
    void itemColorChanged();
    void lineColorChanged();
    void textButtonTriggered();
    void fillButtonTriggered();
    void lineButtonTriggered();
    void handleFontChange();
    void itemSelected(QGraphicsItem *item);
    void about();

private:
    void createToolBox();
    void createActions();
    void createMenus();
    void createToolbars();
    QWidget *createBackgroundCellWidget(const QString &text,
                                        const QString &image);
    QWidget *createCellWidget(const QString &text,
                              DiagramItem::DiagramType type);

    template<typename PointerToMemberFunction>
    QMenu *createColorMenu(const PointerToMemberFunction &slot, QColor defaultColor);
    QIcon createColorToolButtonIcon(const QString &image, QColor color);
    QIcon createColorIcon(QColor color);

    DiagramScene *scene;
    QGraphicsView *view;

    QAction *exitAction;
    QAction *addAction;
    QAction *deleteAction;

    QAction *toFrontAction;
    QAction *sendBackAction;
    QAction *aboutAction;

    QMenu *fileMenu;
    QMenu *itemMenu;
    QMenu *aboutMenu;

    QToolBar *textToolBar;
    QToolBar *editToolBar;
    QToolBar *colorToolBar;
    QToolBar *pointerToolbar;

    QComboBox *sceneScaleCombo;
    QComboBox *itemColorCombo;
    QComboBox *textColorCombo;
    QComboBox *fontSizeCombo;
    QFontComboBox *fontCombo;

    QToolBox *toolBox;
    QButtonGroup *buttonGroup;
    QButtonGroup *pointerTypeGroup;
    QButtonGroup *backgroundButtonGroup;
    QToolButton *fontColorToolButton;
    QToolButton *fillColorToolButton;
    QToolButton *lineColorToolButton;
    QAction *boldAction;
    QAction *underlineAction;
    QAction *italicAction;
    QAction *textAction;
    QAction *fillAction;
    QAction *lineAction;
};

La classe MainWindow crée et dispose les widgets dans une fenêtre QMainWindow. La classe transmet les données des widgets à la DiagramScene. Elle met également à jour ses widgets lorsque l'élément de texte de la scène de diagramme change, ou lorsqu'un élément de diagramme ou un élément de texte de diagramme est inséré dans la scène.

La classe supprime également des éléments de la scène et gère l'ordre z, qui détermine l'ordre dans lequel les éléments sont dessinés lorsqu'ils se chevauchent.

Mise en œuvre de la classe MainWindow

Nous commençons par examiner le constructeur :

MainWindow::MainWindow()
{
    createActions();
    createToolBox();
    createMenus();

    scene = new DiagramScene(itemMenu, this);
    scene->setSceneRect(QRectF(0, 0, 5000, 5000));
    connect(scene, &DiagramScene::itemInserted,
            this, &MainWindow::itemInserted);
    connect(scene, &DiagramScene::textInserted,
            this, &MainWindow::textInserted);
    connect(scene, &DiagramScene::itemSelected,
            this, &MainWindow::itemSelected);
    createToolbars();

    QHBoxLayout *layout = new QHBoxLayout;
    layout->addWidget(toolBox);
    view = new QGraphicsView(scene);
    layout->addWidget(view);

    QWidget *widget = new QWidget;
    widget->setLayout(layout);

    setCentralWidget(widget);
    setWindowTitle(tr("Diagramscene"));
    setUnifiedTitleAndToolBarOnMac(true);
}

Dans le constructeur, nous appelons des méthodes pour créer les widgets et les dispositions de l'exemple avant de créer la scène du diagramme. Les barres d'outils doivent être créées après la scène car elles sont connectées à ses signaux. Nous disposons ensuite les widgets dans la fenêtre.

Nous nous connectons aux emplacements itemInserted() et textInserted() des scènes de diagramme car nous voulons décocher les boutons de la boîte à outils lorsqu'un élément est inséré. Lorsqu'un élément est sélectionné dans la scène, nous recevons le signal itemSelected(). Nous l'utilisons pour mettre à jour les widgets qui affichent les propriétés des polices si l'élément sélectionné est un DiagramTextItem.

La fonction createToolBox() crée et met en place les widgets de la fonction toolBox QToolBox . Nous ne l'examinerons pas en détail, car elle ne traite pas des fonctionnalités propres au cadre graphique. Voici sa mise en œuvre :

void MainWindow::createToolBox()
{
    buttonGroup = new QButtonGroup(this);
    buttonGroup->setExclusive(false);
    connect(buttonGroup, QOverload<QAbstractButton *>::of(&QButtonGroup::buttonClicked),
            this, &MainWindow::buttonGroupClicked);
    QGridLayout *layout = new QGridLayout;
    layout->addWidget(createCellWidget(tr("Conditional"), DiagramItem::Conditional), 0, 0);
    layout->addWidget(createCellWidget(tr("Process"), DiagramItem::Step),0, 1);
    layout->addWidget(createCellWidget(tr("Input/Output"), DiagramItem::Io), 1, 0);

Cette partie de la fonction configure l'élément widget à onglets qui contient les formes de l'organigramme. Un QButtonGroup exclusif garde toujours un bouton coché ; nous voulons que le groupe permette à tous les boutons d'être décochés. Nous utilisons toujours un groupe de boutons car nous pouvons associer des données utilisateur, que nous utilisons pour stocker le type de diagramme, à chaque bouton. La fonction createCellWidget() configure les boutons de l'élément du widget à onglets et sera examinée ultérieurement.

Les boutons de l'élément de widget à onglets d'arrière-plan sont configurés de la même manière, nous passons donc à la création de la boîte à outils :

    toolBox = new QToolBox;
    toolBox->setSizePolicy(QSizePolicy(QSizePolicy::Maximum, QSizePolicy::Ignored));
    toolBox->setMinimumWidth(itemWidget->sizeHint().width());
    toolBox->addItem(itemWidget, tr("Basic Flowchart Shapes"));
    toolBox->addItem(backgroundWidget, tr("Backgrounds"));
}

Nous fixons la taille préférée de la boîte à outils à son maximum. De cette façon, l'affichage graphique occupe plus d'espace.

Voici la fonction createActions():

void MainWindow::createActions()
{
    toFrontAction = new QAction(QIcon(":/images/bringtofront.png"),
                                tr("Bring to &Front"), this);
    toFrontAction->setShortcut(tr("Ctrl+F"));
    toFrontAction->setStatusTip(tr("Bring item to front"));
    connect(toFrontAction, &QAction::triggered, this, &MainWindow::bringToFront);

Nous montrons un exemple de création d'une action. La fonctionnalité déclenchée par les actions est abordée dans les emplacements auxquels les actions sont reliées.

Voici la fonction createMenus():

void MainWindow::createMenus()
{
    fileMenu = menuBar()->addMenu(tr("&File"));
    fileMenu->addAction(exitAction);

    itemMenu = menuBar()->addMenu(tr("&Item"));
    itemMenu->addAction(deleteAction);
    itemMenu->addSeparator();
    itemMenu->addAction(toFrontAction);
    itemMenu->addAction(sendBackAction);

    aboutMenu = menuBar()->addMenu(tr("&Help"));
    aboutMenu->addAction(aboutAction);
}

Nous créons les trois menus de l'exemple.

La fonction createToolbars() met en place les barres d'outils de l'exemple. Les trois QToolButtonde colorToolBar, fontColorToolButton, fillColorToolButton et lineColorToolButton sont intéressants car nous créons des icônes pour eux en dessinant sur un QPixmap avec un QPainter. Nous montrons comment le fillColorToolButton est créé. Ce bouton permet à l'utilisateur de sélectionner une couleur pour les éléments du diagramme.

void MainWindow::createToolbars()
{
    ...
    fillColorToolButton = new QToolButton;
    fillColorToolButton->setPopupMode(QToolButton::MenuButtonPopup);
    fillColorToolButton->setMenu(createColorMenu(&MainWindow::itemColorChanged, Qt::white));
    fillAction = fillColorToolButton->menu()->defaultAction();
    fillColorToolButton->setIcon(createColorToolButtonIcon(
                                     ":/images/floodfill.png", Qt::white));
    connect(fillColorToolButton, &QAbstractButton::clicked,
            this, &MainWindow::fillButtonTriggered);

Nous définissons le menu du bouton d'outil avec setMenu(). L'objet fillAction QAction doit toujours pointer vers l'action sélectionnée dans le menu. Le menu est créé avec la fonction createColorMenu() et, comme nous le verrons plus tard, contient un élément de menu pour chaque couleur que les éléments peuvent avoir. Lorsque l'utilisateur appuie sur le bouton, ce qui déclenche le signal clicked(), nous pouvons définir la couleur de l'élément sélectionné comme étant la couleur de fillAction. C'est avec createColorToolButtonIcon() que nous créons l'icône du bouton.

    ...
}

Voici la fonction createBackgroundCellWidget():

QWidget *MainWindow::createBackgroundCellWidget(const QString &text, const QString &image)
{
    QToolButton *button = new QToolButton;
    button->setText(text);
    button->setIcon(QIcon(image));
    button->setIconSize(QSize(50, 50));
    button->setCheckable(true);
    backgroundButtonGroup->addButton(button);

    QGridLayout *layout = new QGridLayout;
    layout->addWidget(button, 0, 0, Qt::AlignHCenter);
    layout->addWidget(new QLabel(text), 1, 0, Qt::AlignCenter);

    QWidget *widget = new QWidget;
    widget->setLayout(layout);

    return widget;
}

Cette fonction crée des QWidgetcontenant un bouton d'outil et une étiquette. Les widgets créés avec cette fonction sont utilisés pour l'élément de widget à onglet d'arrière-plan dans la boîte à outils.

Voici la fonction createCellWidget():

QWidget *MainWindow::createCellWidget(const QString &text, DiagramItem::DiagramType type)
{

    DiagramItem item(type, itemMenu);
    QIcon icon(item.image());

    QToolButton *button = new QToolButton;
    button->setIcon(icon);
    button->setIconSize(QSize(50, 50));
    button->setCheckable(true);
    buttonGroup->addButton(button, int(type));

    QGridLayout *layout = new QGridLayout;
    layout->addWidget(button, 0, 0, Qt::AlignHCenter);
    layout->addWidget(new QLabel(text), 1, 0, Qt::AlignCenter);

    QWidget *widget = new QWidget;
    widget->setLayout(layout);

    return widget;
}

Cette fonction renvoie un QWidget contenant un QToolButton avec une image de l'un des DiagramItems, c'est-à-dire des formes d'organigramme. L'image est créée par la fonction DiagramItem via la fonction image(). La classe QButtonGroup nous permet d'associer un identifiant (int) à chaque bouton ; nous stockons le type de diagramme, c'est-à-dire l'enum DiagramItem::DiagramType. Nous utilisons le type de diagramme stocké lorsque nous créons de nouveaux éléments de diagramme pour la scène. Les widgets créés avec cette fonction sont utilisés dans la boîte à outils.

Voici la fonction createColorMenu():

template<typename PointerToMemberFunction>
QMenu *MainWindow::createColorMenu(const PointerToMemberFunction &slot, QColor defaultColor)
{
    QList<QColor> colors;
    colors << Qt::black << Qt::white << Qt::red << Qt::blue << Qt::yellow;
    QStringList names;
    names << tr("black") << tr("white") << tr("red") << tr("blue")
          << tr("yellow");

    QMenu *colorMenu = new QMenu(this);
    for (int i = 0; i < colors.count(); ++i) {
        QAction *action = new QAction(names.at(i), this);
        action->setData(colors.at(i));
        action->setIcon(createColorIcon(colors.at(i)));
        connect(action, &QAction::triggered, this, slot);
        colorMenu->addAction(action);
        if (colors.at(i) == defaultColor)
            colorMenu->setDefaultAction(action);
    }
    return colorMenu;
}

Cette fonction crée un menu de couleurs qui est utilisé comme menu déroulant pour les boutons d'outils dans le site colorToolBar. Nous créons une action pour chaque couleur que nous ajoutons au menu. Nous récupérons les données des actions lorsque nous définissons la couleur des éléments, des lignes et du texte.

Voici la fonction createColorToolButtonIcon():

QIcon MainWindow::createColorToolButtonIcon(const QString &imageFile, QColor color)
{
    QPixmap pixmap(50, 80);
    pixmap.fill(Qt::transparent);
    QPainter painter(&pixmap);
    QPixmap image(imageFile);
    // Draw icon centred horizontally on button.
    QRect target(4, 0, 42, 43);
    QRect source(0, 0, 42, 43);
    painter.fillRect(QRect(0, 60, 50, 80), color);
    painter.drawPixmap(target, image, source);

    return QIcon(pixmap);
}

Cette fonction est utilisée pour créer les QIcon des fillColorToolButton, fontColorToolButton et lineColorToolButton. La chaîne imageFile est le texte, le remplissage ou le symbole de ligne utilisé pour les boutons. Sous l'image, nous dessinons un rectangle rempli à l'aide de color.

Voici la fonction createColorIcon():

QIcon MainWindow::createColorIcon(QColor color)
{
    QPixmap pixmap(20, 20);
    QPainter painter(&pixmap);
    painter.setPen(Qt::NoPen);
    painter.fillRect(QRect(0, 0, 20, 20), color);

    return QIcon(pixmap);
}

Cette fonction crée une icône avec un rectangle rempli dans la couleur de color. Elle est utilisée pour créer des icônes pour les menus de couleur dans les sites fillColorToolButton, fontColorToolButton, et lineColorToolButton.

Voici l'emplacement backgroundButtonGroupClicked():

void MainWindow::backgroundButtonGroupClicked(QAbstractButton *button)
{
    const QList<QAbstractButton *> buttons = backgroundButtonGroup->buttons();
    for (QAbstractButton *myButton : buttons) {
        if (myButton != button)
            button->setChecked(false);
    }
    QString text = button->text();
    if (text == tr("Blue Grid"))
        scene->setBackgroundBrush(QPixmap(":/images/background1.png"));
    else if (text == tr("White Grid"))
        scene->setBackgroundBrush(QPixmap(":/images/background2.png"));
    else if (text == tr("Gray Grid"))
        scene->setBackgroundBrush(QPixmap(":/images/background3.png"));
    else
        scene->setBackgroundBrush(QPixmap(":/images/background4.png"));

    scene->update();
    view->update();
}

Dans cette fonction, nous définissons le QBrush qui est utilisé pour dessiner l'arrière-plan de la scène de diagramme. L'arrière-plan peut être une grille de carreaux bleus, gris ou blancs, ou pas de grille du tout. Nous disposons de QPixmaps de carreaux provenant de fichiers png avec lesquels nous créons la brosse.

Lorsque l'on clique sur l'un des boutons de l'élément du widget à onglets de l'arrière-plan, nous changeons la brosse ; nous découvrons de quel bouton il s'agit en vérifiant son texte.

Voici l'implémentation de buttonGroupClicked():

void MainWindow::buttonGroupClicked(QAbstractButton *button)
{
    const QList<QAbstractButton *> buttons = buttonGroup->buttons();
    for (QAbstractButton *myButton : buttons) {
        if (myButton != button)
            button->setChecked(false);
    }
    const int id = buttonGroup->id(button);
    if (id == InsertTextButton) {
        scene->setMode(DiagramScene::InsertText);
    } else {
        scene->setItemType(DiagramItem::DiagramType(id));
        scene->setMode(DiagramScene::InsertItem);
    }
}

Ce slot est appelé lorsqu'un bouton de buttonGroup est coché. Lorsqu'un bouton est coché, l'utilisateur peut cliquer sur la vue graphique et un DiagramItem du type sélectionné sera inséré dans le site DiagramScene. Nous devons parcourir en boucle les boutons du groupe pour décocher les autres boutons, car un seul bouton peut être coché à la fois.

QButtonGroup attribue un identifiant à chaque bouton. Nous avons défini l'identifiant de chaque bouton au type de diagramme, tel qu'indiqué par DiagramItem::DiagramType, qui sera inséré dans la scène lorsqu'il sera cliqué. Nous pouvons ensuite utiliser l'identifiant du bouton lorsque nous définissons le type de diagramme avec setItemType(). Dans le cas du texte, nous avons attribué un identifiant dont la valeur ne figure pas dans l'énumération DiagramType.

Voici l'implémentation de deleteItem():

void MainWindow::deleteItem()
{
    QList<QGraphicsItem *> selectedItems = scene->selectedItems();
    for (QGraphicsItem *item : std::as_const(selectedItems)) {
        if (item->type() == Arrow::Type) {
            scene->removeItem(item);
            Arrow *arrow = qgraphicsitem_cast<Arrow *>(item);
            arrow->startItem()->removeArrow(arrow);
            arrow->endItem()->removeArrow(arrow);
            delete item;
        }
    }

    selectedItems = scene->selectedItems();
    for (QGraphicsItem *item : std::as_const(selectedItems)) {
         if (item->type() == DiagramItem::Type)
             qgraphicsitem_cast<DiagramItem *>(item)->removeArrows();
         scene->removeItem(item);
         delete item;
     }
}

Ce slot supprime l'élément sélectionné, le cas échéant, de la scène. Il supprime d'abord les flèches afin d'éviter de les supprimer deux fois. Si l'élément à supprimer est un DiagramItem, nous devons également supprimer les flèches qui y sont connectées ; nous ne voulons pas de flèches dans la scène qui ne sont pas connectées à des éléments aux deux extrémités.

Voici l'implémentation de pointerGroupClicked() :

void MainWindow::pointerGroupClicked()
{
    scene->setMode(DiagramScene::Mode(pointerTypeGroup->checkedId()));
}

Le site pointerTypeGroup décide si la scène est en mode ItemMove ou InsertLine. Ce groupe de boutons est exclusif, c'est-à-dire qu'un seul bouton est coché à la fois. Comme pour le buttonGroup ci-dessus, nous avons attribué un identifiant aux boutons qui correspond aux valeurs de l'enum DiagramScene::Mode, de sorte que nous puissions utiliser l'identifiant pour définir le mode correct.

Voici l'emplacement bringToFront():

void MainWindow::bringToFront()
{
    if (scene->selectedItems().isEmpty())
        return;

    QGraphicsItem *selectedItem = scene->selectedItems().first();
    const QList<QGraphicsItem *> overlapItems = selectedItem->collidingItems();

    qreal zValue = 0;
    for (const QGraphicsItem *item : overlapItems) {
        if (item->zValue() >= zValue && item->type() == DiagramItem::Type)
            zValue = item->zValue() + 0.1;
    }
    selectedItem->setZValue(zValue);
}

Plusieurs éléments peuvent entrer en collision, c'est-à-dire se chevaucher, dans la scène. Ce slot est appelé lorsque l'utilisateur demande qu'un élément soit placé au-dessus des éléments avec lesquels il entre en collision. QGrapicsItems possède une valeur z qui détermine l'ordre dans lequel les éléments sont empilés dans la scène ; vous pouvez l'assimiler à l'axe z d'un système de coordonnées 3D. Lorsque des éléments entrent en collision, les éléments ayant une valeur z plus élevée sont dessinés au-dessus des éléments ayant une valeur plus faible. Lorsque nous plaçons un élément au premier plan, nous pouvons parcourir en boucle les éléments avec lesquels il entre en collision et définir une valeur z supérieure à chacune d'entre elles.

Voici l'emplacement sendToBack():

void MainWindow::sendToBack()
{
    if (scene->selectedItems().isEmpty())
        return;

    QGraphicsItem *selectedItem = scene->selectedItems().first();
    const QList<QGraphicsItem *> overlapItems = selectedItem->collidingItems();

    qreal zValue = 0;
    for (const QGraphicsItem *item : overlapItems) {
        if (item->zValue() <= zValue && item->type() == DiagramItem::Type)
            zValue = item->zValue() - 0.1;
    }
    selectedItem->setZValue(zValue);
}

Ce slot fonctionne de la même manière que bringToFront() décrit ci-dessus, mais fixe une valeur z inférieure aux éléments avec lesquels l'élément qui doit être envoyé à l'arrière entre en collision.

Il s'agit de l'implémentation de itemInserted():

void MainWindow::itemInserted(DiagramItem *item)
{
    pointerTypeGroup->button(int(DiagramScene::MoveItem))->setChecked(true);
    scene->setMode(DiagramScene::Mode(pointerTypeGroup->checkedId()));
    buttonGroup->button(int(item->diagramType()))->setChecked(false);
}

Ce slot est appelé depuis DiagramScene lorsqu'un élément a été ajouté à la scène. Nous ramenons le mode de la scène au mode précédant l'insertion de l'élément, c'est-à-dire ItemMove ou InsertText en fonction du bouton coché dans pointerTypeGroup. Nous devons également décocher le bouton dans buttonGroup.

Voici la mise en œuvre de textInserted():

void MainWindow::textInserted(QGraphicsTextItem *)
{
    buttonGroup->button(InsertTextButton)->setChecked(false);
    scene->setMode(DiagramScene::Mode(pointerTypeGroup->checkedId()));
}

Nous ramenons simplement le mode de la scène au mode qu'il avait avant l'insertion du texte.

Voici l'emplacement currentFontChanged():

void MainWindow::currentFontChanged(const QFont &)
{
    handleFontChange();
}

Lorsque l'utilisateur demande un changement de police, en utilisant l'un des widgets de fontToolBar, nous créons un nouvel objet QFont et définissons ses propriétés pour qu'elles correspondent à l'état des widgets. Cette opération est effectuée dans handleFontChange(), et nous appelons donc simplement ce slot.

Voici le slot fontSizeChanged():

void MainWindow::fontSizeChanged(const QString &)
{
    handleFontChange();
}

Lorsque l'utilisateur demande un changement de police, en utilisant l'un des widgets de fontToolBar, nous créons un nouvel objet QFont et définissons ses propriétés pour qu'elles correspondent à l'état des widgets. Ceci est fait dans handleFontChange(), donc nous appelons simplement ce slot.

Voici la mise en œuvre de sceneScaleChanged():

void MainWindow::sceneScaleChanged(const QString &scale)
{
    double newScale = scale.left(scale.indexOf(tr("%"))).toDouble() / 100.0;
    QTransform oldMatrix = view->transform();
    view->resetTransform();
    view->translate(oldMatrix.dx(), oldMatrix.dy());
    view->scale(newScale, newScale);
}

L'utilisateur peut augmenter ou diminuer l'échelle, avec sceneScaleCombo, la scène est dessinée. Ce n'est pas la scène elle-même qui change d'échelle, mais seulement la vue.

Voici le slot textColorChanged():

void MainWindow::textColorChanged()
{
    textAction = qobject_cast<QAction *>(sender());
    fontColorToolButton->setIcon(createColorToolButtonIcon(
                                     ":/images/textpointer.png",
                                     qvariant_cast<QColor>(textAction->data())));
    textButtonTriggered();
}

Ce slot est appelé lorsqu'un élément du menu déroulant de fontColorToolButton est pressé. Nous devons changer l'icône du bouton pour qu'elle soit de la couleur de l'élément sélectionné QAction. Nous gardons un pointeur sur l'action sélectionnée dans textAction. C'est dans textButtonTriggered() que nous changeons la couleur du texte pour la couleur de textAction, nous appelons donc ce slot.

Voici l'implémentation de itemColorChanged():

void MainWindow::itemColorChanged()
{
    fillAction = qobject_cast<QAction *>(sender());
    fillColorToolButton->setIcon(createColorToolButtonIcon(
                                     ":/images/floodfill.png",
                                     qvariant_cast<QColor>(fillAction->data())));
    fillButtonTriggered();
}

Ce slot gère les demandes de changement de couleur de DiagramItems de la même manière que textColorChanged() le fait pour DiagramTextItems.

Voici l'implémentation de lineColorChanged():

void MainWindow::lineColorChanged()
{
    lineAction = qobject_cast<QAction *>(sender());
    lineColorToolButton->setIcon(createColorToolButtonIcon(
                                     ":/images/linecolor.png",
                                     qvariant_cast<QColor>(lineAction->data())));
    lineButtonTriggered();
}

Ce slot gère les demandes de changement de couleur de Arrows de la même manière que textColorChanged() le fait pour DiagramTextItems.

Voici le slot textButtonTriggered():

void MainWindow::textButtonTriggered()
{
    scene->setTextColor(qvariant_cast<QColor>(textAction->data()));
}

textAction pointe sur QAction de l'élément de menu actuellement sélectionné dans le menu déroulant des couleurs de fontColorToolButton. Nous avons défini les données de l'action à l'adresse QColor que l'action représente, de sorte que nous pouvons simplement la récupérer lorsque nous définissons la couleur du texte à l'aide de setTextColor().

Voici l'emplacement fillButtonTriggered():

void MainWindow::fillButtonTriggered()
{
    scene->setItemColor(qvariant_cast<QColor>(fillAction->data()));
}

fillAction pointe sur l'élément de menu sélectionné dans le menu déroulant de fillColorToolButton(). Nous pouvons donc utiliser les données de cette action lorsque nous définissons la couleur de l'élément avec setItemColor().

Voici le slot lineButtonTriggered():

void MainWindow::lineButtonTriggered()
{
    scene->setLineColor(qvariant_cast<QColor>(lineAction->data()));
}

lineAction pointe sur l'élément sélectionné dans le menu déroulant de lineColorToolButton. Nous utilisons ses données lorsque nous définissons la couleur de la flèche avec setLineColor().

Voici la fonction handleFontChange():

void MainWindow::handleFontChange()
{
    QFont font = fontCombo->currentFont();
    font.setPointSize(fontSizeCombo->currentText().toInt());
    font.setWeight(boldAction->isChecked() ? QFont::Bold : QFont::Normal);
    font.setItalic(italicAction->isChecked());
    font.setUnderline(underlineAction->isChecked());

    scene->setFont(font);
}

handleFontChange() est appelée lorsque l'un des widgets affichant les propriétés de la police change. Nous créons un nouvel objet QFont et définissons ses propriétés en fonction des widgets. Nous appelons ensuite la fonction setFont() de DiagramScene; c'est la scène qui définit la police de caractères de l'objet DiagramTextItems qu'elle gère.

Voici le slot itemSelected():

void MainWindow::itemSelected(QGraphicsItem *item)
{
    DiagramTextItem *textItem =
    qgraphicsitem_cast<DiagramTextItem *>(item);

    QFont font = textItem->font();
    fontCombo->setCurrentFont(font);
    fontSizeCombo->setEditText(QString().setNum(font.pointSize()));
    boldAction->setChecked(font.weight() == QFont::Bold);
    italicAction->setChecked(font.italic());
    underlineAction->setChecked(font.underline());
}

Ce slot est appelé lorsqu'un élément du site DiagramScene est sélectionné. Dans le cas de cet exemple, seuls les éléments textuels émettent des signaux lorsqu'ils sont sélectionnés, nous n'avons donc pas besoin de vérifier le type de graphique de item.

Nous définissons l'état des widgets pour qu'ils correspondent aux propriétés de la police de l'élément de texte sélectionné.

Voici l'emplacement about():

void MainWindow::about()
{
    QMessageBox::about(this, tr("About Diagram Scene"),
                       tr("The <b>Diagram Scene</b> example shows "
                          "use of the graphics framework."));
}

Ce slot affiche une boîte "about" pour l'exemple lorsque l'utilisateur sélectionne l'élément "about" dans le menu d'aide.

Définition de la classe DiagramScene

La classe DiagramScene hérite de QGraphicsScene et ajoute des fonctionnalités pour gérer DiagramItems, Arrows et DiagramTextItems en plus des éléments gérés par sa super-classe.

class DiagramScene : public QGraphicsScene
{
    Q_OBJECT

public:
    enum Mode { InsertItem, InsertLine, InsertText, MoveItem };

    explicit DiagramScene(QMenu *itemMenu, QObject *parent = nullptr);
    QFont font() const { return myFont; }
    QColor textColor() const { return myTextColor; }
    QColor itemColor() const { return myItemColor; }
    QColor lineColor() const { return myLineColor; }
    void setLineColor(const QColor &color);
    void setTextColor(const QColor &color);
    void setItemColor(const QColor &color);
    void setFont(const QFont &font);

public slots:
    void setMode(Mode mode);
    void setItemType(DiagramItem::DiagramType type);
    void editorLostFocus(DiagramTextItem *item);

signals:
    void itemInserted(DiagramItem *item);
    void textInserted(QGraphicsTextItem *item);
    void itemSelected(QGraphicsItem *item);

protected:
    void mousePressEvent(QGraphicsSceneMouseEvent *mouseEvent) override;
    void mouseMoveEvent(QGraphicsSceneMouseEvent *mouseEvent) override;
    void mouseReleaseEvent(QGraphicsSceneMouseEvent *mouseEvent) override;

private:
    bool isItemChange(int type) const;

    DiagramItem::DiagramType myItemType;
    QMenu *myItemMenu;
    Mode myMode;
    bool leftButtonDown;
    QPointF startPoint;
    QGraphicsLineItem *line;
    QFont myFont;
    DiagramTextItem *textItem;
    QColor myTextColor;
    QColor myItemColor;
    QColor myLineColor;
};

Dans DiagramScene, un clic de souris peut donner lieu à trois actions différentes : l'élément sous la souris peut être déplacé, un élément peut être inséré ou une flèche peut être connectée entre deux éléments du diagramme. L'action d'un clic de souris dépend du mode, donné par l'énumération Mode, dans lequel se trouve la scène. Le mode est défini par la fonction setMode().

La scène définit également la couleur de ses éléments et la police de ses éléments de texte. Les couleurs et la police utilisées par la scène peuvent être définies à l'aide des fonctions setLineColor(), setTextColor(), setItemColor() et setFont(). Le type de DiagramItem, donné par la fonction DiagramItem::DiagramType, à créer lorsqu'un élément est inséré est défini avec le slot setItemType().

Les fonctions MainWindow et DiagramScene se partagent la responsabilité des exemples. MainWindow gère les tâches suivantes : la suppression d'éléments, de texte et de flèches ; le déplacement d'éléments de diagramme vers l'arrière et vers l'avant ; et le réglage de l'échelle de la scène.

Mise en œuvre de la classe DiagramScene

Nous commençons par le constructeur :

DiagramScene::DiagramScene(QMenu *itemMenu, QObject *parent)
    : QGraphicsScene(parent)
{
    myItemMenu = itemMenu;
    myMode = MoveItem;
    myItemType = DiagramItem::Step;
    line = nullptr;
    textItem = nullptr;
    myItemColor = Qt::white;
    myTextColor = Qt::black;
    myLineColor = Qt::black;
}

La scène utilise myItemMenu pour définir le menu contextuel lorsqu'elle crée DiagramItems. Nous définissons le mode par défaut sur DiagramScene::MoveItem car cela donne le comportement par défaut de QGraphicsScene.

Voici la fonction setLineColor():

void DiagramScene::setLineColor(const QColor &color)
{
    myLineColor = color;
    if (isItemChange(Arrow::Type)) {
        Arrow *item = qgraphicsitem_cast<Arrow *>(selectedItems().first());
        item->setColor(myLineColor);
        update();
    }
}

La fonction isItemChange renvoie un message vrai si un élément Arrow est sélectionné dans la scène, auquel cas nous voulons changer sa couleur. Lorsque DiagramScene crée et ajoute de nouvelles flèches à la scène, il utilise également la nouvelle fonction color.

Voici la fonction setTextColor():

void DiagramScene::setTextColor(const QColor &color)
{
    myTextColor = color;
    if (isItemChange(DiagramTextItem::Type)) {
        DiagramTextItem *item = qgraphicsitem_cast<DiagramTextItem *>(selectedItems().first());
        item->setDefaultTextColor(myTextColor);
    }
}

Cette fonction définit la couleur de DiagramTextItems de la même manière que setLineColor() définit la couleur de Arrows.

Voici la fonction setItemColor():

void DiagramScene::setItemColor(const QColor &color)
{
    myItemColor = color;
    if (isItemChange(DiagramItem::Type)) {
        DiagramItem *item = qgraphicsitem_cast<DiagramItem *>(selectedItems().first());
        item->setBrush(myItemColor);
    }
}

Cette fonction définit la couleur que la scène utilisera lors de la création de DiagramItems. Elle modifie également la couleur d'un objet sélectionné DiagramItem.

Voici l'implémentation de setFont():

void DiagramScene::setFont(const QFont &font)
{
    myFont = font;

    if (isItemChange(DiagramTextItem::Type)) {
        QGraphicsTextItem *item = qgraphicsitem_cast<DiagramTextItem *>(selectedItems().first());
        //At this point the selection can change so the first selected item might not be a DiagramTextItem
        if (item)
            item->setFont(myFont);
    }
}

Définir la police à utiliser pour les nouveaux éléments et les éléments sélectionnés, si un élément de texte est sélectionné, DiagramTextItems.

Il s'agit de l'implémentation de editorLostFocus() slot :

void DiagramScene::editorLostFocus(DiagramTextItem *item)
{
    QTextCursor cursor = item->textCursor();
    cursor.clearSelection();
    item->setTextCursor(cursor);

    if (item->toPlainText().isEmpty()) {
        removeItem(item);
        item->deleteLater();
    }
}

DiagramTextItems émettent un signal lorsqu'ils perdent le focus, qui est connecté à ce slot. Nous supprimons l'élément s'il n'a pas de texte. Si ce n'était pas le cas, il y aurait une fuite de mémoire et l'utilisateur serait dérouté, car les éléments seront édités lorsqu'on appuiera sur la souris.

La fonction mousePressEvent() gère les événements de pression de la souris différemment selon le mode dans lequel se trouve le site DiagramScene. Nous examinons sa mise en œuvre pour chaque mode :

void DiagramScene::mousePressEvent(QGraphicsSceneMouseEvent *mouseEvent)
{
    if (mouseEvent->button() != Qt::LeftButton)
        return;

    DiagramItem *item;
    switch (myMode) {
        case InsertItem:
            item = new DiagramItem(myItemType, myItemMenu);
            item->setBrush(myItemColor);
            addItem(item);
            item->setPos(mouseEvent->scenePos());
            emit itemInserted(item);
            break;

Nous créons simplement un nouveau DiagramItem et l'ajoutons à la scène à l'endroit où la souris a été pressée. Notez que l'origine de son système de coordonnées local se trouve sous la position du pointeur de la souris.

        case InsertLine:
            line = new QGraphicsLineItem(QLineF(mouseEvent->scenePos(),
                                        mouseEvent->scenePos()));
            line->setPen(QPen(myLineColor, 2));
            addItem(line);
            break;

L'utilisateur ajoute Arrows à la scène en étirant une ligne entre les éléments que la flèche doit relier. Le début de la ligne est fixé à l'endroit où l'utilisateur a cliqué sur la souris et l'extrémité suit le pointeur de la souris tant que le bouton est maintenu enfoncé. Lorsque l'utilisateur relâche le bouton de la souris, une adresse Arrow est ajoutée à la scène s'il existe une adresse DiagramItem sous le début et la fin de la ligne. Nous verrons plus tard comment cela est mis en œuvre ; ici, nous ajoutons simplement la ligne.

        case InsertText:
            textItem = new DiagramTextItem();
            textItem->setFont(myFont);
            textItem->setTextInteractionFlags(Qt::TextEditorInteraction);
            textItem->setZValue(1000.0);
            connect(textItem, &DiagramTextItem::lostFocus,
                    this, &DiagramScene::editorLostFocus);
            connect(textItem, &DiagramTextItem::selectedChange,
                    this, &DiagramScene::itemSelected);
            addItem(textItem);
            textItem->setDefaultTextColor(myTextColor);
            textItem->setPos(mouseEvent->scenePos());
            emit textInserted(textItem);

Le DiagramTextItem est modifiable lorsque le drapeau Qt::TextEditorInteraction est activé, sinon il est déplaçable à l'aide de la souris. Nous voulons toujours que le texte soit dessiné au-dessus des autres éléments de la scène, c'est pourquoi nous fixons la valeur à un nombre supérieur à celui des autres éléments de la scène.

    default:
        ;
    }
    QGraphicsScene::mousePressEvent(mouseEvent);
}

Nous sommes en mode MoveItem si nous atteignons le commutateur par défaut ; nous pouvons alors appeler l'implémentation QGraphicsScene, qui gère le déplacement des éléments à l'aide de la souris. Nous faisons cet appel même si nous sommes dans un autre mode, ce qui permet d'ajouter un élément, de maintenir le bouton de la souris enfoncé et de commencer à déplacer l'élément. Dans le cas des éléments textuels, cela n'est pas possible car ils ne propagent pas d'événements souris lorsqu'ils sont modifiables.

Il s'agit de la fonction mouseMoveEvent():

void DiagramScene::mouseMoveEvent(QGraphicsSceneMouseEvent *mouseEvent)
{
    if (myMode == InsertLine && line != nullptr) {
        QLineF newLine(line->line().p1(), mouseEvent->scenePos());
        line->setLine(newLine);
    } else if (myMode == MoveItem) {
        QGraphicsScene::mouseMoveEvent(mouseEvent);
    }
}

Nous devons tracer la ligne si nous sommes en mode Insertion et que le bouton de la souris est enfoncé (la ligne n'est pas 0). Comme indiqué à l'adresse mousePressEvent(), la ligne est tracée à partir de la position où la souris a été enfoncée jusqu'à la position actuelle de la souris.

Si nous sommes en mode MoveItem, nous appelons l'implémentation QGraphicsScene, qui gère le déplacement des éléments.

Dans la fonction mouseReleaseEvent(), nous devons vérifier si une flèche doit être ajoutée à la scène :

void DiagramScene::mouseReleaseEvent(QGraphicsSceneMouseEvent *mouseEvent)
{
    if (line != nullptr && myMode == InsertLine) {
        QList<QGraphicsItem *> startItems = items(line->line().p1());
        if (startItems.count() && startItems.first() == line)
            startItems.removeFirst();
        QList<QGraphicsItem *> endItems = items(line->line().p2());
        if (endItems.count() && endItems.first() == line)
            endItems.removeFirst();

        removeItem(line);
        delete line;

Tout d'abord, nous devons récupérer les éléments (s'il y en a) situés sous les points de départ et d'arrivée de la ligne. La ligne elle-même est le premier élément à ces points, nous la supprimons donc des listes. Par précaution, nous vérifions si les listes sont vides, ce qui ne devrait jamais se produire.

        if (startItems.count() > 0 && endItems.count() > 0 &&
            startItems.first()->type() == DiagramItem::Type &&
            endItems.first()->type() == DiagramItem::Type &&
            startItems.first() != endItems.first()) {
            DiagramItem *startItem = qgraphicsitem_cast<DiagramItem *>(startItems.first());
            DiagramItem *endItem = qgraphicsitem_cast<DiagramItem *>(endItems.first());
            Arrow *arrow = new Arrow(startItem, endItem);
            arrow->setColor(myLineColor);
            startItem->addArrow(arrow);
            endItem->addArrow(arrow);
            arrow->setZValue(-1000.0);
            addItem(arrow);
            arrow->updatePosition();
        }
    }

Nous vérifions maintenant s'il y a deux DiagramItems différents sous les points de départ et d'arrivée des lignes. Si c'est le cas, nous pouvons créer un Arrow avec les deux éléments. La flèche est ensuite ajoutée à chaque élément et enfin à la scène. La flèche doit être mise à jour pour ajuster ses points de départ et d'arrivée aux éléments. Nous fixons la valeur z de la flèche à -1000,0 car nous voulons qu'elle soit toujours dessinée sous les objets.

    line = nullptr;
    QGraphicsScene::mouseReleaseEvent(mouseEvent);
}

Voici la fonction isItemChange():

bool DiagramScene::isItemChange(int type) const
{
    const QList<QGraphicsItem *> items = selectedItems();
    const auto cb = [type](const QGraphicsItem *item) { return item->type() == type; };
    return std::find_if(items.begin(), items.end(), cb) != items.end();
}

La scène est à sélection unique, c'est-à-dire qu'un seul élément peut être sélectionné à un moment donné. La boucle for boucle alors une fois avec l'élément sélectionné ou aucune fois si aucun élément n'est sélectionné. isItemChange() est utilisé pour vérifier si un élément sélectionné existe et s'il fait partie du diagramme spécifié type.

Définition de la classe DiagramItem

class DiagramItem : public QGraphicsPolygonItem
{
public:
    enum { Type = UserType + 15 };
    enum DiagramType { Step, Conditional, StartEnd, Io };

    DiagramItem(DiagramType diagramType, QMenu *contextMenu, QGraphicsItem *parent = nullptr);

    void removeArrow(Arrow *arrow);
    void removeArrows();
    DiagramType diagramType() const { return myDiagramType; }
    QPolygonF polygon() const { return myPolygon; }
    void addArrow(Arrow *arrow);
    QPixmap image() const;
    int type() const override { return Type; }

protected:
    void contextMenuEvent(QGraphicsSceneContextMenuEvent *event) override;
    QVariant itemChange(GraphicsItemChange change, const QVariant &value) override;

private:
    DiagramType myDiagramType;
    QPolygonF myPolygon;
    QMenu *myContextMenu;
    QList<Arrow *> arrows;
};

La classe DiagramItem représente une forme d'organigramme dans le diagramme DiagramScene. Elle hérite de QGraphicsPolygonItem et possède un polygone pour chaque forme. L'énumération DiagramType contient une valeur pour chaque forme d'organigramme.

La classe possède une liste des flèches qui lui sont connectées. Cette liste est nécessaire car seul l'élément sait quand il est déplacé (à l'aide de la fonction itemChanged() ) et à quel moment les flèches doivent être mises à jour. L'élément peut également se dessiner sur un site QPixmap à l'aide de la fonction image(). Cette fonction est utilisée pour les boutons d'outils dans MainWindow, voir createColorToolButtonIcon() dans MainWindow.

L'énumération Type est un identifiant unique de la classe. Elle est utilisée par qgraphicsitem_cast(), qui effectue des moulages dynamiques d'éléments graphiques. La constante UserType est la valeur minimale que peut avoir un type d'élément graphique personnalisé.

Mise en œuvre de la classe DiagramItem

Nous commençons par examiner le constructeur :

DiagramItem::DiagramItem(DiagramType diagramType, QMenu *contextMenu,
                         QGraphicsItem *parent)
    : QGraphicsPolygonItem(parent), myDiagramType(diagramType)
    , myContextMenu(contextMenu)
{
    QPainterPath path;
    switch (myDiagramType) {
        case StartEnd:
            path.moveTo(200, 50);
            path.arcTo(150, 0, 50, 50, 0, 90);
            path.arcTo(50, 0, 50, 50, 90, 90);
            path.arcTo(50, 50, 50, 50, 180, 90);
            path.arcTo(150, 50, 50, 50, 270, 90);
            path.lineTo(200, 25);
            myPolygon = path.toFillPolygon();
            break;
        case Conditional:
            myPolygon << QPointF(-100, 0) << QPointF(0, 100)
                      << QPointF(100, 0) << QPointF(0, -100)
                      << QPointF(-100, 0);
            break;
        case Step:
            myPolygon << QPointF(-100, -100) << QPointF(100, -100)
                      << QPointF(100, 100) << QPointF(-100, 100)
                      << QPointF(-100, -100);
            break;
        default:
            myPolygon << QPointF(-120, -80) << QPointF(-70, 80)
                      << QPointF(120, 80) << QPointF(70, -80)
                      << QPointF(-120, -80);
            break;
    }
    setPolygon(myPolygon);
    setFlag(QGraphicsItem::ItemIsMovable, true);
    setFlag(QGraphicsItem::ItemIsSelectable, true);
    setFlag(QGraphicsItem::ItemSendsGeometryChanges, true);
}

Dans le constructeur, nous créons le polygone de l'élément conformément à diagramType. Les éléments QGraphicsItemne sont pas déplaçables ou sélectionnables par défaut, nous devons donc définir ces propriétés.

Voici la fonction removeArrow():

void DiagramItem::removeArrow(Arrow *arrow)
{
    arrows.removeAll(arrow);
}

removeArrow() est utilisée pour supprimer les éléments Arrow lorsque ceux-ci ou DiagramItems auxquels ils sont connectés sont supprimés de la scène.

Voici la fonction removeArrows():

void DiagramItem::removeArrows()
{
    // need a copy here since removeArrow() will
    // modify the arrows container
    const auto arrowsCopy = arrows;
    for (Arrow *arrow : arrowsCopy) {
        arrow->startItem()->removeArrow(arrow);
        arrow->endItem()->removeArrow(arrow);
        scene()->removeItem(arrow);
        delete arrow;
    }
}

Cette fonction est appelée lorsque l'élément est retiré de la scène et supprime toutes les flèches qui sont connectées à cet élément. La flèche doit être supprimée de la liste arrows de son élément de départ et de son élément d'arrivée. Comme l'élément de départ ou d'arrivée est l'objet où cette fonction est actuellement appelée, nous devons nous assurer de travailler sur une copie des flèches puisque removeArrow() modifie ce conteneur.

Voici la fonction addArrow():

void DiagramItem::addArrow(Arrow *arrow)
{
    arrows.append(arrow);
}

Cette fonction ajoute simplement l'élément arrow à la liste des éléments arrows.

Voici la fonction image():

QPixmap DiagramItem::image() const
{
    QPixmap pixmap(250, 250);
    pixmap.fill(Qt::transparent);
    QPainter painter(&pixmap);
    painter.setPen(QPen(Qt::black, 8));
    painter.translate(125, 125);
    painter.drawPolyline(myPolygon);

    return pixmap;
}

Cette fonction dessine le polygone de l'élément sur un QPixmap. Dans cet exemple, nous l'utilisons pour créer des icônes pour les boutons d'outils dans la boîte à outils.

Voici la fonction contextMenuEvent():

void DiagramItem::contextMenuEvent(QGraphicsSceneContextMenuEvent *event)
{
    scene()->clearSelection();
    setSelected(true);
    myContextMenu->popup(event->screenPos());
}

Nous affichons le menu contextuel. Comme le clic droit de la souris, qui affiche le menu, ne sélectionne pas les éléments par défaut, nous définissons l'élément sélectionné à l'aide de setSelected(). Ceci est nécessaire puisqu'un élément doit être sélectionné pour changer son élévation avec les actions bringToFront et sendToBack.

Il s'agit de l'implémentation de itemChange():

QVariant DiagramItem::itemChange(GraphicsItemChange change, const QVariant &value)
{
    if (change == QGraphicsItem::ItemPositionChange) {
        for (Arrow *arrow : std::as_const(arrows))
            arrow->updatePosition();
    }

    return value;
}

Si l'élément a été déplacé, nous devons mettre à jour les positions des flèches qui y sont reliées. L'implémentation de QGraphicsItem ne fait rien, nous renvoyons donc simplement value.

Définition de la classe DiagramTextItem

La classe TextDiagramItem hérite de QGraphicsTextItem et ajoute la possibilité de déplacer des éléments de texte modifiables. Les QGraphicsTextItems éditables sont conçus pour être fixés en place et l'édition commence lorsque l'utilisateur clique sur l'élément. Avec DiagramTextItem, l'édition commence par un double clic, ce qui laisse un simple clic pour interagir avec l'élément et le déplacer.

class DiagramTextItem : public QGraphicsTextItem
{
    Q_OBJECT

public:
    enum { Type = UserType + 3 };

    DiagramTextItem(QGraphicsItem *parent = nullptr);

    int type() const override { return Type; }

signals:
    void lostFocus(DiagramTextItem *item);
    void selectedChange(QGraphicsItem *item);

protected:
    QVariant itemChange(GraphicsItemChange change, const QVariant &value) override;
    void focusOutEvent(QFocusEvent *event) override;
    void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override;
};

Nous utilisons itemChange() et focusOutEvent() pour avertir DiagramScene lorsque l'élément de texte perd le focus et est sélectionné.

Nous réimplémentons les fonctions qui gèrent les événements de la souris afin de pouvoir modifier le comportement de la souris sur QGraphicsTextItem.

Mise en œuvre de DiagramTextItem

Nous commençons par le constructeur :

DiagramTextItem::DiagramTextItem(QGraphicsItem *parent)
    : QGraphicsTextItem(parent)
{
    setFlag(QGraphicsItem::ItemIsMovable);
    setFlag(QGraphicsItem::ItemIsSelectable);
}

Nous définissons simplement l'élément mobile et sélectionnable, car ces drapeaux sont désactivés par défaut.

Voici la fonction itemChange():

QVariant DiagramTextItem::itemChange(GraphicsItemChange change,
                     const QVariant &value)
{
    if (change == QGraphicsItem::ItemSelectedHasChanged)
        emit selectedChange(this);
    return value;
}

Lorsque l'élément est sélectionné, nous émettons le signal selectedChanged. Le site MainWindow utilise ce signal pour mettre à jour les widgets qui affichent les propriétés de police en fonction de la police de l'élément de texte sélectionné.

Voici la fonction focusOutEvent():

void DiagramTextItem::focusOutEvent(QFocusEvent *event)
{
    setTextInteractionFlags(Qt::NoTextInteraction);
    emit lostFocus(this);
    QGraphicsTextItem::focusOutEvent(event);
}

DiagramScene utilise le signal émis lorsque l'élément de texte perd le focus pour supprimer l'élément s'il est vide, c'est-à-dire s'il ne contient pas de texte.

Voici l'implémentation de mouseDoubleClickEvent():

void DiagramTextItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
{
    if (textInteractionFlags() == Qt::NoTextInteraction)
        setTextInteractionFlags(Qt::TextEditorInteraction);
    QGraphicsTextItem::mouseDoubleClickEvent(event);
}

Lorsque nous recevons un événement de double clic, nous rendons l'élément modifiable en appelant QGraphicsTextItem::setTextInteractionFlags(). Nous transmettons ensuite le double-clic à l'élément lui-même.

Définition de la classe Flèche

La classe Arrow est un élément graphique qui relie deux DiagramItems. Elle dessine une tête de flèche vers l'un des éléments. Pour ce faire, l'élément doit se peindre lui-même et réimplémenter les méthodes utilisées par la scène graphique pour vérifier les collisions et les sélections. La classe hérite de l'élément QGraphicsLine, dessine la pointe de la flèche et se déplace avec les éléments qu'elle relie.

class Arrow : public QGraphicsLineItem
{
public:
    enum { Type = UserType + 4 };

    Arrow(DiagramItem *startItem, DiagramItem *endItem,
          QGraphicsItem *parent = nullptr);

    int type() const override { return Type; }
    QRectF boundingRect() const override;
    QPainterPath shape() const override;
    void setColor(const QColor &color) { myColor = color; }
    DiagramItem *startItem() const { return myStartItem; }
    DiagramItem *endItem() const { return myEndItem; }

    void updatePosition();

protected:
    void paint(QPainter *painter, const QStyleOptionGraphicsItem *option,
               QWidget *widget = nullptr) override;

private:
    DiagramItem *myStartItem;
    DiagramItem *myEndItem;
    QPolygonF arrowHead;
    QColor myColor = Qt::black;
};

La couleur de l'élément peut être définie à l'aide de setColor().

boundingRect() et shape() sont réimplémentés à partir de QGraphicsLineItem et sont utilisés par la scène pour vérifier les collisions et les sélections.

L'appel à updatePosition() entraîne le recalcul de la position et de l'angle de la tête de la flèche. paint() est réimplémenté afin que nous puissions peindre une flèche plutôt qu'une simple ligne entre les éléments.

myStartItem et myEndItem sont les éléments du diagramme que la flèche relie. La flèche est dessinée avec sa tête vers l'élément final. arrowHead est un polygone à trois sommets que nous utilisons pour dessiner la tête de la flèche.

Mise en œuvre de la classe Flèche

Le constructeur de la classe Arrow ressemble à ceci :

Arrow::Arrow(DiagramItem *startItem, DiagramItem *endItem, QGraphicsItem *parent)
    : QGraphicsLineItem(parent), myStartItem(startItem), myEndItem(endItem)
{
    setFlag(QGraphicsItem::ItemIsSelectable, true);
    setPen(QPen(myColor, 2, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
}

Nous définissons les éléments de diagramme de départ et d'arrivée de la flèche. La tête de la flèche sera dessinée à l'endroit où la ligne croise l'élément de fin.

Voici la fonction boundingRect():

QRectF Arrow::boundingRect() const
{
    qreal extra = (pen().width() + 20) / 2.0;

    return QRectF(line().p1(), QSizeF(line().p2().x() - line().p1().x(),
                                      line().p2().y() - line().p1().y()))
        .normalized()
        .adjusted(-extra, -extra, extra, extra);
}

Nous devons réimplémenter cette fonction car la flèche est plus grande que le rectangle de délimitation de la classe QGraphicsLineItem. La scène graphique utilise le rectangle de délimitation pour savoir quelles régions de la scène doivent être mises à jour.

Voici la fonction shape():

QPainterPath Arrow::shape() const
{
    QPainterPath path = QGraphicsLineItem::shape();
    path.addPolygon(arrowHead);
    return path;
}

La fonction shape renvoie une adresse QPainterPath qui correspond à la forme exacte de l'élément. La fonction QGraphicsLineItem::shape() renvoie un chemin avec une ligne dessinée avec le stylo actuel, de sorte qu'il suffit d'ajouter la tête de la flèche. Cette fonction est utilisée pour vérifier les collisions et les sélections avec la souris.

Voici le slot updatePosition():

void Arrow::updatePosition()
{
    QLineF line(mapFromItem(myStartItem, 0, 0), mapFromItem(myEndItem, 0, 0));
    setLine(line);
}

Ce slot met à jour la flèche en fixant les points de départ et d'arrivée de sa ligne au centre des éléments qu'elle relie.

Voici la fonction paint():

void Arrow::paint(QPainter *painter, const QStyleOptionGraphicsItem *,
                  QWidget *)
{
    if (myStartItem->collidesWithItem(myEndItem))
        return;

    QPen myPen = pen();
    myPen.setColor(myColor);
    qreal arrowSize = 20;
    painter->setPen(myPen);
    painter->setBrush(myColor);

Si les points de départ et d'arrivée entrent en collision, nous ne dessinons pas la flèche ; l'algorithme que nous utilisons pour trouver le point où la flèche doit être dessinée peut échouer si les éléments entrent en collision.

Nous commençons par définir le stylo et le pinceau que nous utiliserons pour dessiner la flèche.

    QLineF centerLine(myStartItem->pos(), myEndItem->pos());
    QPolygonF endPolygon = myEndItem->polygon();
    QPointF p1 = endPolygon.first() + myEndItem->pos();
    QPointF intersectPoint;
    for (int i = 1; i < endPolygon.count(); ++i) {
        QPointF p2 = endPolygon.at(i) + myEndItem->pos();
        QLineF polyLine = QLineF(p1, p2);
        QLineF::IntersectionType intersectionType =
            polyLine.intersects(centerLine, &intersectPoint);
        if (intersectionType == QLineF::BoundedIntersection)
            break;
        p1 = p2;
    }

    setLine(QLineF(intersectPoint, myStartItem->pos()));

Nous devons ensuite trouver la position à laquelle dessiner la tête de la flèche. La tête doit être dessinée à l'intersection de la ligne et de l'élément final. Pour ce faire, nous prenons la ligne entre chaque point du polygone et vérifions si elle croise la ligne de la flèche. Comme les points de départ et d'arrivée de la ligne sont fixés au centre des éléments, la ligne de la flèche doit croiser une et une seule des lignes du polygone. Notez que les points du polygone sont relatifs au système de coordonnées local de l'élément. Nous devons donc ajouter la position de l'élément final pour que les coordonnées soient relatives à la scène.

    double angle = std::atan2(-line().dy(), line().dx());

    QPointF arrowP1 = line().p1() + QPointF(sin(angle + M_PI / 3) * arrowSize,
                                    cos(angle + M_PI / 3) * arrowSize);
    QPointF arrowP2 = line().p1() + QPointF(sin(angle + M_PI - M_PI / 3) * arrowSize,
                                    cos(angle + M_PI - M_PI / 3) * arrowSize);

    arrowHead.clear();
    arrowHead << line().p1() << arrowP1 << arrowP2;

Nous calculons l'angle entre l'axe x et la ligne de la flèche. Nous devons tourner la tête de la flèche selon cet angle afin qu'elle suive la direction de la flèche. Si l'angle est négatif, nous devons tourner la direction de la flèche.

Nous pouvons alors calculer les trois points du polygone de la tête de flèche. L'un de ces points est l'extrémité de la ligne, qui est maintenant l'intersection entre la ligne de la flèche et le polygone d'extrémité. Nous effaçons ensuite le polygone arrowHead de la tête de flèche calculée précédemment et définissons ces nouveaux points.

    painter->drawLine(line());
    painter->drawPolygon(arrowHead);
    if (isSelected()) {
        painter->setPen(QPen(myColor, 1, Qt::DashLine));
        QLineF myLine = line();
        myLine.translate(0, 4.0);
        painter->drawLine(myLine);
        myLine.translate(0,-8.0);
        painter->drawLine(myLine);
    }
}

Si la ligne est sélectionnée, nous traçons deux lignes en pointillés parallèles à la ligne de la flèche. Nous n'utilisons pas l'implémentation par défaut, qui utilise boundingRect() parce que le rectangle de délimitation de QRect est considérablement plus grand que la ligne.

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.