Exemple de scène de diagramme
Démonstration de l'utilisation du framework Graphics View.

L'exemple « Diagram Scene » est une application qui vous permet de créer un organigramme. Il est possible d'ajouter des formes d'organigramme et du texte, puis de relier ces formes à l'aide de 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 framework « Graphics View » de Qt est conçu pour gérer et afficher des éléments graphiques 2D personnalisés. Les principales classes de ce framework sont ` QGraphicsItem`, ` QGraphicsScene ` et ` QGraphicsView`. La scène graphique gère les éléments et leur fournit une surface d’affichage. ` QGraphicsView ` est un widget utilisé pour afficher une scène à l’écran. Consultez la documentation du framework « Graphics View » pour une description plus détaillée de ce framework.
Dans cet exemple, nous montrons comment créer de telles scènes graphiques et de tels éléments personnalisés en implémentant des classes qui héritent de ` QGraphicsScene ` et ` QGraphicsItem`.
Nous montrons notamment comment :
- Créer des éléments graphiques personnalisés.
- Gérer les événements de souris et les déplacements des éléments.
- Implémenter une scène graphique capable de gérer nos éléments personnalisés.
- Personnaliser le rendu des éléments.
- Créer un élément de texte déplaçable et modifiable.
L'exemple se compose des classes suivantes :
MainWindowcrée les widgets et les affiche dans une QMainWindow. Elle gère également l'interaction entre les widgets et la scène graphique, la vue et les éléments.DiagramItemhérite de ` QGraphicsPolygonItem ` et représente une forme d’organigramme.TextDiagramItemhérite de ` QGraphicsTextItem ` et représente les éléments de texte dans le diagramme. Cette classe permet de déplacer l’élément à l’aide de la souris, fonctionnalité non prise en charge par ` QGraphicsTextItem`.ArrowHérite de ` QGraphicsLineItem ` et correspond à une flèche reliant deux `DiagramItems`.DiagramSceneHé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 organise les widgets dans une QMainWindow. Elle transmet les entrées provenant 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 de superposition (z-ordering), qui détermine l’ordre dans lequel les éléments sont dessinés lorsqu’ils se chevauchent.
Implémentation de la classe MainWindow
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 de diagramme. Les barres d’outils doivent être créées après la scène, car elles se connectent à ses signaux. Nous disposons ensuite les widgets dans la fenêtre.
Nous nous connectons aux slots ` itemInserted() ` et ` textInserted() ` de la scène de diagramme, car nous souhaitons 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 de police si l’élément sélectionné est un ` DiagramTextItem`.
La fonction ` createToolBox() ` crée et organise les widgets de l’ QToolBox` toolBox `. Nous ne l’examinerons pas en détail, car elle ne traite pas de fonctionnalités spécifiques au framework graphique. Voici son implémentation :
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 le widget à onglets qui contient les formes de l’organigramme. Un QButtonGroup exclusif maintient toujours un bouton coché ; nous voulons que le groupe permette de décocher tous les boutons. Nous utilisons toujours un groupe de boutons, car nous pouvons associer à chaque bouton des données utilisateur, que nous utilisons pour stocker le type de diagramme. La fonction ` createCellWidget() ` configure les boutons dans le widget à onglets et sera examinée plus loin.
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 directement à 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 définissons la taille préférée de la boîte à outils comme sa taille maximale. De cette manière, davantage d’espace est alloué à la vue graphique.
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 présentons ici un exemple de création d’une action. La fonctionnalité déclenchée par les actions est abordée dans les slots auxquels nous connectons ces actions.
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() ` configure les barres d’outils de l’exemple. Les trois ` QToolButton` dans ` colorToolBar`, ` fontColorToolButton`, ` fillColorToolButton` et ` lineColorToolButton` sont intéressants, car nous créons des icônes pour celles-ci en dessinant sur un ` QPixmap ` à l’aide d’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 configurons le menu du bouton de l’outil à l’aide de la fonction setMenu(). L’objet fillAction QAction doit toujours pointer vers l’action sélectionnée du menu. Le menu est créé à l’aide de la fonction createColorMenu() et, comme nous le verrons plus tard, contient un élément de menu pour chaque couleur que les éléments peuvent prendre. 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é sur 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 à l’aide de cette fonction sont utilisés pour l’élément de widget à onglets 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 objet ` QWidget ` contenant un ` QToolButton ` avec une image correspondant à l'une des formes du type ` DiagramItems`, c'est-à-dire les formes d'organigramme. L'image est créée par la classe ` DiagramItem ` via la fonction ` image() `. La classe QButtonGroup permet d’associer un identifiant (int) à chaque bouton ; nous y stockons le type de diagramme, c’est-à-dire l’énumération 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 à l’aide de 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 sert de menu déroulant pour les boutons d’outils de l’ colorToolBar. Nous créons une action pour chaque couleur que nous ajoutons au menu. Nous récupérons les données relatives à ces 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 sert à créer l’ QIcon des éléments fillColorToolButton, fontColorToolButton et lineColorToolButton. La chaîne imageFile correspond au symbole « texte », « remplissage » ou « 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 de la couleur indiquée par color. Elle sert à créer des icônes pour les menus de couleurs dans les fichiers fillColorToolButton, fontColorToolButton et lineColorToolButton.
Voici le slot « 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 paramètre « QBrush » qui sert à dessiner l’arrière-plan de la scène de diagramme. L’arrière-plan peut être une grille de carrés composés de tuiles bleues, grises ou blanches, ou ne comporter aucune grille. Nous disposons d’ QPixmaps de ces tuiles issues de fichiers PNG à partir desquelles nous créons le pinceau.
Lorsqu’on clique sur l’un des boutons du widget à onglets « arrière-plan », on change de pinceau ; on identifie le bouton concerné en vérifiant son texte.
Voici l’implémentation de l’ 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 une DiagramItem du type sélectionné sera insérée dans l’ DiagramScene. Nous devons parcourir 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 en fonction du type de diagramme, tel qu’indiqué par `DiagramItem::DiagramType`, qui sera inséré dans la scène lorsqu’on cliquera dessus. 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é, s’il y en a un, 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 soient pas connectées à des éléments à leurs deux extrémités.
Voici l’implémentation de la fonction pointerGroupClicked() :
void MainWindow::pointerGroupClicked()
{
scene->setMode(DiagramScene::Mode(pointerTypeGroup->checkedId()));
}L’ pointerTypeGroup détermine 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 l’ buttonGroup ci-dessus, nous avons attribué aux boutons un identifiant correspondant aux valeurs de l’énumération DiagramScene::Mode, afin de pouvoir utiliser cet identifiant pour définir le mode approprié.
Voici le slot « 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, les uns avec les autres 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. Les éléments « QGrapicsItems » possèdent une valeur z qui détermine l’ordre dans lequel les éléments sont empilés dans la scène ; vous pouvez le considérer comme l’axe z dans un système de coordonnées 3D. Lorsque des éléments entrent en collision, ceux dont la valeur z est la plus élevée s’affichent par-dessus ceux dont la valeur est inférieure. Lorsque nous amenons un élément au premier plan, nous pouvons parcourir 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 définit une valeur z inférieure à celle des éléments avec lesquels l’élément à envoyer à l’arrière entre en collision.
Voici 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 la méthode ` DiagramScene ` lorsqu’un élément a été ajouté à la scène. Nous rétablissons le mode de la scène tel qu’il était avant l’insertion de l’élément, c’est-à-dire `ItemMove` ou `InsertText` selon le bouton coché dans la méthode ` pointerTypeGroup`. Nous devons également décocher le bouton dans la méthode ` buttonGroup`.
Voici l’implémentation de la méthode ` textInserted()` :
void MainWindow::textInserted(QGraphicsTextItem *)
{
buttonGroup->button(InsertTextButton)->setChecked(false);
scene->setMode(DiagramScene::Mode(pointerTypeGroup->checkedId()));
}Nous rétablissons simplement le mode de la scène tel qu’il était avant l’insertion du texte.
Voici le slot « currentFontChanged() » :
void MainWindow::currentFontChanged(const QFont &)
{
handleFontChange();
}Lorsque l’utilisateur demande un changement de police, à l’aide de l’un des widgets de l’ fontToolBar, nous créons un nouvel objet QFont et définissons ses propriétés pour qu’elles correspondent à l’état des widgets. Cela se fait dans handleFontChange(), nous appelons donc simplement ce slot.
Voici le slot « fontSizeChanged() » :
void MainWindow::fontSizeChanged(const QString &)
{
handleFontChange();
}Lorsque l’utilisateur demande un changement de police, à l’aide de l’un des widgets de l’ 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 s’effectue dans handleFontChange(), nous appelons donc simplement ce slot.
Voici l’implémentation 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 agrandir ou réduire l'échelle ; grâce à la méthode ` sceneScaleCombo`, la scène est redessinée. Ce n'est pas la scène elle-même qui change d'échelle, mais uniquement 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 l’ fontColorToolButton est sélectionné. Nous devons changer la couleur de l’icône du bouton pour qu’elle corresponde à celle de l’ QAction sélectionnée. Nous conservons un pointeur vers l’action sélectionnée dans textAction. C’est dans textButtonTriggered() que nous changeons la couleur du texte pour qu’elle corresponde à celle 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 requêtes de modification de la 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 modification de la 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 renvoie vers l’ 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 sur l’ QColor que l’action représente, ce qui nous permet de la récupérer simplement lorsque nous définissons la couleur du texte avec setTextColor().
Voici le slot « fillButtonTriggered() » :
void MainWindow::fillButtonTriggered()
{
scene->setItemColor(qvariant_cast<QColor>(fillAction->data()));
}fillAction renvoie à l’élément 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 à l’aide de « setItemColor() ».
Voici le slot « lineButtonTriggered() » :
void MainWindow::lineButtonTriggered()
{
scene->setLineColor(qvariant_cast<QColor>(lineAction->data()));
}lineAction il pointe vers 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() Elle est appelée lorsque l’un des widgets affichant les propriétés de 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 l’ 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 de l’ DiagramScene est sélectionné. Dans le cas de cet exemple, seuls les éléments de texte émettent des signaux lorsqu’ils sont sélectionnés ; nous n’avons donc pas besoin de vérifier de quel type d’ item graphique il s’agit.
Nous définissons l’état des widgets afin qu’il corresponde aux propriétés de la police de l’élément de texte sélectionné.
Voici le slot « 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 de dialogue « À propos » pour l’exemple lorsque l’utilisateur sélectionne l’élément de menu « À propos » dans le menu « Aide ».
Définition de la classe DiagramScene
La classe ` DiagramScene ` hérite de ` QGraphicsScene ` et ajoute des fonctionnalités permettant de gérer ` DiagramItems`, ` Arrows` et ` DiagramTextItems `, en plus des éléments gérés par sa classe parente.
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 l’ DiagramScene, un clic de souris peut déclencher trois actions différentes : l’élément sous le curseur peut être déplacé, un élément peut être inséré, ou une flèche peut être tracée entre deux éléments du diagramme. L’action déclenchée par un clic de souris dépend du mode, défini par l’énumération Mode, dans lequel se trouve la scène. Le mode est défini à l’aide de la fonction ` setMode() `.
La scène définit également la couleur de ses éléments et la police de ses éléments textuels. 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 d’ DiagramItem, déterminé par la fonction `DiagramItem::DiagramType`, à créer lors de l’insertion d’un élément est défini via le slot ` setItemType() `.
Les classes ` MainWindow ` et ` DiagramScene ` se partagent la responsabilité des fonctionnalités d’exemples. La classe ` MainWindow ` gère les tâches suivantes : la suppression d’éléments, de texte et de flèches ; le déplacement d’éléments du diagramme vers l’arrière-plan ou le premier plan ; et le réglage de l’échelle de la scène.
Implémentation de la classe DiagramScene
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 `true` si un élément ` Arrow ` est sélectionné dans la scène ; dans ce cas, nous souhaitons modifier sa couleur. Lorsque la fonction ` DiagramScene ` crée et ajoute de nouvelles flèches à la scène, elle utilise également la nouvelle valeur de ` 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 d'DiagramItems. Elle modifie également la couleur d'un DiagramItem sélectionné.
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éfinit la police à utiliser pour les éléments nouveaux et sélectionnés, si un élément de texte est sélectionné, DiagramTextItems.
Voici l’implémentation du slot « editorLostFocus() » :
void DiagramScene::editorLostFocus(DiagramTextItem *item)
{
QTextCursor cursor = item->textCursor();
cursor.clearSelection();
item->setTextCursor(cursor);
if (item->toPlainText().isEmpty()) {
removeItem(item);
item->deleteLater();
}
}DiagramTextItems émettre un signal lorsqu’ils perdent le focus, lequel est connecté à ce slot. Nous supprimons l’élément s’il ne contient pas de texte. Sinon, cela entraînerait une fuite de mémoire et dérouterait l’utilisateur, car les éléments seraient modifiés lorsqu’on cliquerait dessus avec la souris.
La fonction ` mousePressEvent() ` gère les événements de clic de souris différemment selon le mode dans lequel se trouve l’ DiagramScene. Examinons son implémentation 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 nouvel « DiagramItem » et l’ajoutons à la scène à l’endroit où la souris a été cliqué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 une « Arrows » à la scène en traçant une ligne entre les éléments que la flèche doit relier. Le point de départ de la ligne est fixé à l’endroit où l’utilisateur a cliqué avec la souris et le point d’arrivée suit le pointeur de la souris tant que le bouton reste enfoncé. Lorsque l’utilisateur relâche le bouton de la souris, une « Arrow » sera ajoutée à la scène s’il y a une « DiagramItem » sous le point de départ et le point d’arrivée de la ligne. Nous verrons plus tard comment cela est implémenté ; ici, nous nous contentons d’ajouter 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);L’ DiagramTextItem est modifiable lorsque l’indicateur « Qt::TextEditorInteraction » est activé ; sinon, elle peut être déplacée à l’aide de la souris. Nous souhaitons toujours que le texte s’affiche par-dessus les autres éléments de la scène ; nous définissons donc sa valeur sur un nombre supérieur à celui des autres éléments de la scène.
default:
;
}
QGraphicsScene::mousePressEvent(mouseEvent);
}Nous sommes en mode « MoveItem » si nous arrivons au commutateur par défaut ; nous pouvons alors appeler l’implémentation de ` QGraphicsScene `, qui gère le déplacement des éléments à l’aide de la souris. Nous effectuons cet appel même si nous sommes dans un autre mode, ce qui permet d’ajouter un élément, puis de maintenir le bouton de la souris enfoncé et de commencer à déplacer l’élément. Dans le cas des éléments de texte, cela n’est pas possible car ils ne propagent pas les événements de souris lorsqu’ils sont modifiables.
Voici 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 InsertMode et que le bouton de la souris est enfoncé (la ligne n’est pas égale à 0). Comme indiqué dans mousePressEvent(), la ligne est tracée entre la position où la souris a été enfoncée et la position actuelle de la souris.
Si nous sommes en mode « MoveItem », nous appelons l’implémentation de ` 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 (le cas échéant) 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 retirons donc des listes. Par mesure de précaution, nous vérifions si les listes sont vides, mais cela ne devrait jamais arriver.
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 ensuite s’il existe deux ` DiagramItems ` distincts situés respectivement aux points de départ et d’arrivée de la ligne. Si c’est le cas, nous pouvons créer une ` Arrow ` avec ces deux éléments. La flèche est alors ajoutée à chaque élément, puis à la scène. La flèche doit être mise à jour afin d’ajuster ses points de départ et d’arrivée en fonction des éléments. Nous définissons la valeur z de la flèche à -1000,0 car nous voulons qu’elle soit toujours dessinée sous les éléments.
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 dispose d’une sélection unique, c’est-à-dire qu’un seul élément peut être sélectionné à un moment donné. La boucle « for » s’exécutera donc une fois avec l’élément sélectionné, ou pas du tout si aucun élément n’est sélectionné. La fonction « isItemChange() » est utilisée pour vérifier si un élément sélectionné existe et s’il appartient bien à la type de diagramme spécifiée.
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 la classe ` QGraphicsPolygonItem ` et dispose d’un polygone pour chaque forme. L’énumération `DiagramType` possède une valeur pour chacune des formes d’organigramme.
La classe dispose d’une liste des flèches qui lui sont associées. Cela est nécessaire car seul l’élément sait quand il est déplacé (à l’aide de la fonction ` itemChanged() `), moment auquel les flèches doivent être mises à jour. L’élément peut également s’afficher lui-même sur un ` QPixmap ` à l’aide de la fonction ` image() `. Cette fonctionnalité 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 conversions dynamiques d'éléments graphiques. La constante UserType correspond à la valeur minimale qu'un type d'élément graphique personnalisé peut prendre.
Implémentation de la classe DiagramItem
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. Par défaut, les QGraphicsItems ne sont ni déplaçables ni sélectionnables ; nous devons donc définir ces propriétés.
Voici la fonction ` removeArrow() ` :
void DiagramItem::removeArrow(Arrow *arrow)
{
arrows.removeAll(arrow);
}removeArrow() Elle sert à supprimer les éléments Arrow lorsqu’ils-mêmes ou les 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 supprimé de la scène et supprime toutes les flèches connectées à cet élément. La flèche doit être supprimée de la liste d'arrows s de ses éléments de départ et d'arrivée. Étant donné que l'élément de départ ou d'arrivée est l'objet sur lequel cette fonction est actuellement appelée, nous devons veiller à travailler sur une copie des flèches, car removeArrow() modifie ce conteneur.
Voici la fonction ` addArrow() ` :
void DiagramItem::addArrow(Arrow *arrow)
{
arrows.append(arrow);
}Cette fonction ajoute simplement l'arrow à la liste arrows des éléments.
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 de 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 les clics droits de la souris, qui affichent le menu, ne sélectionnent pas d’éléments par défaut, nous définissons l’élément sélectionné à l’aide de setSelected(). Cela est nécessaire car un élément doit être sélectionné pour modifier son élévation avec les actions bringToFront et sendToBack.
Voici 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 nous contentons donc de renvoyer ` 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 éléments `QGraphicsTextItem` modifiables sont conçus pour rester fixes à leur emplacement et l’édition commence lorsque l’utilisateur clique une fois sur l’élément. Avec ` DiagramTextItem `, l’édition commence par un double-clic, ce qui laisse le simple clic disponible 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 signaler à l’ 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 souris afin de permettre de modifier le comportement de la souris de QGraphicsTextItem.
Implémentation de DiagramTextItem
Commençons par le constructeur :
DiagramTextItem::DiagramTextItem(QGraphicsItem *parent)
: QGraphicsTextItem(parent)
{
setFlag(QGraphicsItem::ItemIsMovable);
setFlag(QGraphicsItem::ItemIsSelectable);
}Nous définissons simplement l’élément comme déplaçable et sélectionnable, car ces indicateurs 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. La fonction 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 cet élément s'il est vide, c'est-à-dire s'il ne contient aucun texte.
Voici l’implémentation de la méthode 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 Arrow
La classe ` Arrow ` est un élément graphique qui relie deux ` DiagramItems`. Elle dessine une pointe de flèche vers l’un des éléments. Pour ce faire, l’élément doit se dessiner 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 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() Les méthodes ` shape() ` sont réimplémentées à partir de ` QGraphicsLineItem ` et sont utilisées par la scène pour vérifier les collisions et les sélections.
L'appel de ` updatePosition() ` provoque le recalcul de la position et de l'angle de la pointe de la flèche. La méthode ` paint() ` est réimplémentée afin de pouvoir dessiner 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 pointe orientée vers l’élément final. arrowHead est un polygone à trois sommets que nous utilisons pour dessiner la pointe de la flèche.
Implémentation de la classe Arrow
Le constructeur de la classe ` Arrow ` se présente comme suit :
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 pointe de la flèche sera dessinée à l'intersection de la ligne et de l'élément d'arrivée.
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 englobant de la classe ` QGraphicsLineItem`. La scène graphique utilise ce rectangle englobant pour déterminer les zones de la scène à mettre à jour.
Voici la fonction ` shape() ` :
QPainterPath Arrow::shape() const
{
QPainterPath path = QGraphicsLineItem::shape();
path.addPolygon(arrowHead);
return path;
}La fonction `shape` renvoie un ` QPainterPath ` qui correspond exactement à la forme de l’élément. La fonction ` QGraphicsLineItem::shape()` renvoie un chemin comportant une ligne dessinée avec le stylo actuel ; il ne nous reste donc plus qu’à ajouter la pointe de la flèche. Cette fonction sert à vérifier les collisions et les sélections à l’aide de 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 définissant 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 éléments de départ et d’arrivée entrent en collision, nous ne dessinons pas la flèche ; l’algorithme que nous utilisons pour déterminer le point où la flèche doit être dessinée peut échouer en cas de collision entre les éléments.
Nous définissons d’abord 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 déterminer la position à laquelle dessiner la pointe de la flèche. La pointe doit être dessinée à l’intersection de la ligne et de l’élément final. Pour ce faire, nous calculons la ligne reliant 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 définis 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 orienter la pointe 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 inverser la direction de la flèche.
Nous pouvons alors calculer les trois points du polygone formant la pointe de la flèche. L’un de ces points correspond à l’extrémité de la ligne, qui est désormais le point d’intersection entre la ligne de la flèche et le polygone de fin. Ensuite, nous effaçons le polygone arrowHead de la pointe 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 pointillées parallèles à la ligne de la flèche. Nous n’utilisons pas l’implémentation par défaut, qui utilise ` boundingRect()`, car le rectangle englobant de ` QRect ` est considérablement plus grand que la ligne.
© 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.