Beispiel für eine Diagrammszene
Veranschaulichung der Verwendung des Graphics View-Frameworks.

Das Beispiel „Diagram Scene“ ist eine Anwendung, mit der Sie ein Flussdiagramm erstellen können. Sie können Flussdiagramm-Formen und Text hinzufügen und die Formen wie in der Abbildung oben durch Pfeile verbinden. Den Formen, Pfeilen und dem Text können verschiedene Farben zugewiesen werden, und es ist möglich, Schriftart, Stil und Unterstreichung des Texts zu ändern.
Das Qt-Graphics-View-Framework dient zur Verwaltung und Darstellung benutzerdefinierter 2D-Grafikelemente. Die Hauptklassen des Frameworks sind „ QGraphicsItem “, „ QGraphicsScene “ und „ QGraphicsView “. Die Graphics-Szene verwaltet die Elemente und stellt eine Oberfläche für sie bereit. „ QGraphicsView “ ist ein Widget, das zum Rendern einer Szene auf dem Bildschirm verwendet wird. Eine ausführlichere Beschreibung des Frameworks finden Sie unter „Graphics View Framework “.
In diesem Beispiel zeigen wir, wie man solche benutzerdefinierten Grafik-Szenen und Elemente erstellt, indem man Klassen implementiert, die von „ QGraphicsScene “ und „ QGraphicsItem “ erben.
Insbesondere zeigen wir, wie man:
- benutzerdefinierte Grafikelemente erstellen;
- Mausereignisse und die Bewegung von Elementen zu verarbeiten.
- Implementierung einer Grafikszene, die unsere benutzerdefinierten Elemente verwalten kann.
- Das benutzerdefinierte Zeichnen von Elementen.
- Erstellen eines verschiebbaren und bearbeitbaren Textelements.
Das Beispiel besteht aus den folgenden Klassen:
MainWindowErstellt die Widgets und zeigt sie in einem ` QMainWindow` an. Außerdem verwaltet sie die Interaktion zwischen den Widgets und der Grafikszene, der Ansicht und den Elementen.DiagramItemerbt von ` QGraphicsPolygonItem ` und stellt eine Flussdiagramm-Form dar.TextDiagramItemerbt von ` QGraphicsTextItem ` und stellt Textelemente im Diagramm dar. Die Klasse bietet die Möglichkeit, das Element mit der Maus zu verschieben – eine Funktion, die von ` QGraphicsTextItem` nicht unterstützt wird.ArrowErbt von ` QGraphicsLineItem ` und ist ein Pfeil, der zwei `DiagramItems` verbindet.DiagramSceneErbt von `QGraphicsDiagramScene` und bietet Unterstützung für `DiagramItem`, `Arrow` und `DiagramTextItem` (zusätzlich zu der bereits von ` QGraphicsScene` bereitgestellten Unterstützung).
Definition der Klasse „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;
};Die Klasse „ MainWindow “ erstellt die Widgets und ordnet sie in einer „ QMainWindow “ an. Die Klasse leitet Eingaben von den Widgets an die „DiagramScene“ weiter. Außerdem aktualisiert sie ihre Widgets, wenn sich das Textelement der Diagrammszene ändert oder ein Diagrammelement oder ein Diagrammtextelement in die Szene eingefügt wird.
Die Klasse löscht zudem Elemente aus der Szene und verwaltet die Z-Reihenfolge, die bestimmt, in welcher Reihenfolge Elemente gezeichnet werden, wenn sie sich überlappen.
Implementierung der Klasse „MainWindow“
Wir beginnen mit einem Blick auf den Konstruktor:
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);
}Im Konstruktor rufen wir Methoden auf, um die Widgets und Layouts des Beispiels zu erstellen, bevor wir die Diagrammszene anlegen. Die Symbolleisten müssen nach der Szene erstellt werden, da sie mit deren Signalen verbunden sind. Anschließend ordnen wir die Widgets im Fenster an.
Wir verbinden uns mit den Slots „ itemInserted() “ und „ textInserted() “ der Diagrammszene, da wir die Schaltflächen in der Werkzeugleiste deaktivieren möchten, wenn ein Element eingefügt wird. Wenn ein Element in der Szene ausgewählt wird, empfangen wir das Signal „ itemSelected() “. Wir nutzen dies, um die Widgets zu aktualisieren, die Schriftarteneigenschaften anzeigen, sofern es sich bei dem ausgewählten Element um ein „ DiagramTextItem “ handelt.
Die Funktion „ createToolBox() “ erstellt und ordnet die Widgets des „ toolBox “ und des „ QToolBox “ an. Wir werden sie nicht im Detail untersuchen, da sie sich nicht mit grafikspezifischen Funktionen befasst. Hier ist ihre Implementierung:
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);Dieser Teil der Funktion richtet das Widget-Element mit Registerkarten ein, das die Flussdiagramm-Formen enthält. Ein exklusives „ QButtonGroup “ hält immer eine Schaltfläche aktiviert; wir möchten jedoch, dass in dieser Gruppe alle Schaltflächen deaktiviert werden können. Wir verwenden weiterhin eine Schaltflächengruppe, da wir jeder Schaltfläche Benutzerdaten zuordnen können, in denen wir den Diagrammtyp speichern. Die Funktion „ createCellWidget() “ richtet die Schaltflächen im Tabbed-Widget-Element ein und wird später näher betrachtet.
Die Schaltflächen des hinteren Widget-Elements mit Registerkarten werden auf dieselbe Weise eingerichtet, daher fahren wir mit der Erstellung der Werkzeugleiste fort:
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"));
}Wir legen die bevorzugte Größe der Werkzeugleiste als deren maximale Größe fest. Auf diese Weise steht mehr Platz für die Grafikansicht zur Verfügung.
Hier ist die Funktion „ 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);Wir zeigen ein Beispiel für die Erstellung einer Aktion. Die Funktionalität, die die Aktionen auslösen, wird in den Slots behandelt, mit denen wir die Aktionen verknüpfen.
Hier ist die Funktion ` 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);
}Wir erstellen die drei Menüs des Beispiels.
Die Funktion „ createToolbars() “ richtet die Symbolleisten des Beispiels ein. Die drei „ QToolButtons“ in den Dateien „ colorToolBar “, „ fontColorToolButton “, „ fillColorToolButton “ und „ lineColorToolButton “ sind interessant, da wir für sie Symbole erstellen, indem wir mit einem „ QPainter “ auf einem „ QPixmap “ zeichnen. Wir zeigen, wie die Schaltfläche „ fillColorToolButton “ erstellt wird. Mit dieser Schaltfläche kann der Benutzer eine Farbe für die Diagrammelemente auswählen.
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);Wir legen das Menü der Werkzeugschaltfläche mit ` setMenu()` fest. Das Objekt ` fillAction ` QAction muss stets auf die ausgewählte Aktion des Menüs verweisen. Das Menü wird mit der Funktion „ createColorMenu() “ erstellt und enthält, wie wir später sehen werden, einen Menüpunkt für jede Farbe, die die Elemente annehmen können. Wenn der Benutzer auf die Schaltfläche drückt, wodurch das Signal „ clicked()“ ausgelöst wird, können wir die Farbe des ausgewählten Elements auf die Farbe von „ fillAction “ setzen. Mit „ createColorToolButtonIcon() “ erstellen wir das Symbol für die Schaltfläche.
...
}Hier ist die Funktion „ 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;
}Diese Funktion erstellt „ QWidgets“, die eine Werkzeugschaltfläche und eine Beschriftung enthalten. Die mit dieser Funktion erstellten Widgets werden für das Hintergrund-Widget mit Registerkarten in der Werkzeugleiste verwendet.
Hier ist die Funktion „ 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;
}Diese Funktion gibt ein ` QWidget ` zurück, das ein ` QToolButton ` mit einem Bild einer der ` DiagramItems` enthält, d. h. Flussdiagrammformen. Das Bild wird von der ` DiagramItem ` mithilfe der Funktion ` image() ` erstellt. Die Klasse „ QButtonGroup “ ermöglicht es uns, jeder Schaltfläche eine ID (int) zuzuweisen; wir speichern den Diagrammtyp, d. h. die Enumeration „DiagramItem::DiagramType“. Wir verwenden den gespeicherten Diagrammtyp, wenn wir neue Diagrammelemente für die Szene erstellen. Die mit dieser Funktion erstellten Widgets werden in der Werkzeugleiste verwendet.
Hier ist die Funktion „ 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;
}Diese Funktion erstellt ein Farbmenü, das als Dropdown-Menü für die Werkzeugschaltflächen im „ colorToolBar “ dient. Wir erstellen für jede Farbe, die wir dem Menü hinzufügen, eine Aktion. Wir rufen die Aktionsdaten ab, wenn wir die Farbe von Elementen, Linien und Text festlegen.
Hier ist die Funktion „ 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);
}Diese Funktion dient zum Erstellen der „ QIcon “ der Elemente „ fillColorToolButton “, „ fontColorToolButton “ und „ lineColorToolButton “. Die Zeichenfolge „ imageFile “ ist entweder das Text-, Füll- oder Liniensymbol, das für die Schaltflächen verwendet wird. Unterhalb des Bildes zeichnen wir mithilfe von „ color “ ein gefülltes Rechteck.
Hier ist die Funktion „ 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);
}Diese Funktion erstellt ein Symbol mit einem gefüllten Rechteck in der Farbe von „ color “. Sie wird zum Erstellen von Symbolen für die Farbmenüs in „ fillColorToolButton “, „ fontColorToolButton “ und „ lineColorToolButton “ verwendet.
Hier ist der 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();
}In dieser Funktion legen wir den „ QBrush “ fest, der zum Zeichnen des Hintergrunds der Diagrammszene verwendet wird. Der Hintergrund kann ein Raster aus blauen, grauen oder weißen Kacheln sein oder gar kein Raster. Wir verfügen über „ QPixmaps“ der Kacheln aus PNG-Dateien, mit denen wir den Pinsel erstellen.
Wenn eine der Schaltflächen im Widget-Element mit Registerkarten im Hintergrund angeklickt wird, wechseln wir den Pinsel; wir ermitteln, um welche Schaltfläche es sich handelt, indem wir deren Text überprüfen.
Hier ist die Implementierung von „ 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);
}
}Dieser Slot wird aufgerufen, wenn eine Schaltfläche in „ buttonGroup “ aktiviert wird. Ist eine Schaltfläche aktiviert, kann der Benutzer auf die Grafikansicht klicken, woraufhin ein „ DiagramItem “ des ausgewählten Typs in das „ DiagramScene “ eingefügt wird. Wir müssen die Schaltflächen in der Gruppe durchlaufen, um andere Schaltflächen zu deaktivieren, da jeweils nur eine Schaltfläche aktiviert sein darf.
QButtonGroup Weist jeder Schaltfläche eine ID zu. Wir haben die ID jeder Schaltfläche auf den Diagrammtyp gesetzt, der durch `DiagramItem::DiagramType` angegeben wird und beim Klicken in die Szene eingefügt wird. Wir können dann die Schaltflächen-ID verwenden, wenn wir den Diagrammtyp mit ` setItemType()` festlegen. Im Falle von Text haben wir eine ID zugewiesen, deren Wert nicht in der Enumeration `DiagramType` enthalten ist.
Hier ist die Implementierung von ` 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;
}
}Dieser Slot löscht das ausgewählte Element, falls vorhanden, aus der Szene. Dabei werden zuerst die Pfeile gelöscht, um zu vermeiden, dass sie doppelt gelöscht werden. Handelt es sich bei dem zu löschenden Element um ein ` DiagramItem`, müssen wir auch die damit verbundenen Pfeile löschen; wir wollen keine Pfeile in der Szene haben, die nicht an beiden Enden mit Elementen verbunden sind.
Dies ist die Implementierung von `pointerGroupClicked()`:
void MainWindow::pointerGroupClicked()
{
scene->setMode(DiagramScene::Mode(pointerTypeGroup->checkedId()));
}Die „ pointerTypeGroup “ entscheidet, ob sich die Szene im Modus „ItemMove“ oder „InsertLine“ befindet. Diese Schaltflächengruppe ist exklusiv, d. h., es ist jeweils nur eine Schaltfläche aktiviert. Wie bei der oben genannten „ buttonGroup “ haben wir den Schaltflächen eine ID zugewiesen, die den Werten der Enumeration „DiagramScene::Mode“ entspricht, sodass wir anhand der ID den richtigen Modus einstellen können.
Hier ist der 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);
}Mehrere Elemente können in der Szene miteinander kollidieren, d. h. sich überlappen. Dieser Slot wird aufgerufen, wenn der Benutzer verlangt, dass ein Element über die Elemente gelegt wird, mit denen es kollidiert. „ QGrapicsItems “ haben einen z-Wert, der die Reihenfolge bestimmt, in der Elemente in der Szene übereinandergeschichtet werden; man kann sich das wie die z-Achse in einem 3D-Koordinatensystem vorstellen. Wenn Objekte kollidieren, werden die Objekte mit höheren z-Werten über den Objekten mit niedrigeren Werten gezeichnet. Wenn wir ein Objekt in den Vordergrund bringen, können wir die Objekte, mit denen es kollidiert, durchlaufen und einen z-Wert festlegen, der höher ist als alle anderen.
Hier ist der Slot „ 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);
}Dieser Slot funktioniert genauso wie das oben beschriebene „ bringToFront() “, legt jedoch einen z-Wert fest, der niedriger ist als die der Objekte, mit denen das Objekt kollidiert, das in den Hintergrund verschoben werden soll.
Dies ist die Implementierung von „ 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);
}Dieser Slot wird von „ DiagramScene “ aufgerufen, wenn ein Element zur Szene hinzugefügt wurde. Wir setzen den Modus der Szene wieder auf den Modus zurück, der vor dem Einfügen des Elements galt – je nachdem, welches Kontrollkästchen in der „ pointerTypeGroup “ aktiviert ist, ist dies entweder „ItemMove“ oder „InsertText“. Außerdem müssen wir das Kontrollkästchen in der „ buttonGroup “ deaktivieren.
Hier ist die Implementierung von „ textInserted() “:
void MainWindow::textInserted(QGraphicsTextItem *)
{
buttonGroup->button(InsertTextButton)->setChecked(false);
scene->setMode(DiagramScene::Mode(pointerTypeGroup->checkedId()));
}Wir setzen den Modus der Szene einfach wieder auf den Modus zurück, den sie vor dem Einfügen des Textes hatte.
Hier ist der Slot „ currentFontChanged() “:
void MainWindow::currentFontChanged(const QFont &)
{
handleFontChange();
}Wenn der Benutzer über eines der Widgets in „ fontToolBar “ eine Schriftartänderung anfordert, erstellen wir ein neues „ QFont “-Objekt und passen dessen Eigenschaften an den Zustand der Widgets an. Dies geschieht in „ handleFontChange() “, daher rufen wir einfach diesen Slot auf.
Hier ist der Slot „ fontSizeChanged() “:
void MainWindow::fontSizeChanged(const QString &)
{
handleFontChange();
}Wenn der Benutzer über eines der Widgets im „ fontToolBar “ eine Schriftartänderung anfordert, erstellen wir ein neues „ QFont “-Objekt und passen dessen Eigenschaften an den Status der Widgets an. Dies geschieht im „ handleFontChange() “, daher rufen wir einfach diesen Slot auf.
Hier ist die Implementierung von ` 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);
}Der Benutzer kann den Maßstab mit dem „ sceneScaleCombo “ vergrößern oder verkleinern, woraufhin die Szene entsprechend gezeichnet wird. Nicht die Szene selbst ändert ihren Maßstab, sondern nur die Ansicht.
Hier ist der Slot „ textColorChanged() “:
void MainWindow::textColorChanged()
{
textAction = qobject_cast<QAction *>(sender());
fontColorToolButton->setIcon(createColorToolButtonIcon(
":/images/textpointer.png",
qvariant_cast<QColor>(textAction->data())));
textButtonTriggered();
}Dieser Slot wird aufgerufen, wenn ein Eintrag im Dropdown-Menü von „ fontColorToolButton “ angeklickt wird. Wir müssen das Symbol auf der Schaltfläche auf die Farbe der ausgewählten Aktion „ QAction “ ändern. Wir speichern einen Zeiger auf die ausgewählte Aktion in „ textAction “. In „ textButtonTriggered() “ ändern wir die Textfarbe auf die Farbe von „ textAction “, daher rufen wir diesen Slot auf.
Hier ist die Implementierung von „ itemColorChanged() “:
void MainWindow::itemColorChanged()
{
fillAction = qobject_cast<QAction *>(sender());
fillColorToolButton->setIcon(createColorToolButtonIcon(
":/images/floodfill.png",
qvariant_cast<QColor>(fillAction->data())));
fillButtonTriggered();
}Dieser Slot verarbeitet Anfragen zur Änderung der Farbe von „ DiagramItems “ auf dieselbe Weise wie „ textColorChanged() “ dies für „ DiagramTextItems “ tut.
Hier ist die Implementierung von lineColorChanged():
void MainWindow::lineColorChanged()
{
lineAction = qobject_cast<QAction *>(sender());
lineColorToolButton->setIcon(createColorToolButtonIcon(
":/images/linecolor.png",
qvariant_cast<QColor>(lineAction->data())));
lineButtonTriggered();
}Dieser Slot verarbeitet Anfragen zur Änderung der Farbe von „ Arrows “ auf dieselbe Weise, wie „ textColorChanged() “ dies für „ DiagramTextItems “ tut.
Hier ist der Slot „ textButtonTriggered() “:
void MainWindow::textButtonTriggered()
{
scene->setTextColor(qvariant_cast<QColor>(textAction->data()));
}textAction verweist auf den „ QAction “ des aktuell ausgewählten Menüpunkts im Farb-Dropdown-Menü von „ fontColorToolButton “. Wir haben die Daten der Aktion auf den „ QColor “ gesetzt, den die Aktion repräsentiert, sodass wir diesen einfach abrufen können, wenn wir die Textfarbe mit „ setTextColor() “ festlegen.
Hier ist der Slot „ fillButtonTriggered() “:
void MainWindow::fillButtonTriggered()
{
scene->setItemColor(qvariant_cast<QColor>(fillAction->data()));
}fillAction verweist auf den ausgewählten Menüpunkt im Dropdown-Menü von „ fillColorToolButton() “. Wir können daher die Daten dieser Aktion verwenden, wenn wir die Farbe des Elements mit „ setItemColor() “ festlegen.
Hier ist der Slot „ lineButtonTriggered() “:
void MainWindow::lineButtonTriggered()
{
scene->setLineColor(qvariant_cast<QColor>(lineAction->data()));
}lineAction Verweist auf den ausgewählten Eintrag im Dropdown-Menü von „ lineColorToolButton “. Wir verwenden dessen Daten, wenn wir die Farbe des Pfeils mit „ setLineColor() “ festlegen.
Hier ist die Funktion „ 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() wird aufgerufen, wenn sich eines der Widgets ändert, die Schriftarteneigenschaften anzeigen. Wir erstellen ein neues „ QFont “-Objekt und legen dessen Eigenschaften basierend auf den Widgets fest. Anschließend rufen wir die Funktion „ setFont() “ von „ DiagramScene “ auf; dies ist die Szene, die die Schriftart des von ihr verwalteten „ DiagramTextItems “ festlegt.
Hier ist der 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());
}Dieser Slot wird aufgerufen, wenn ein Element im „ DiagramScene “ ausgewählt wird. In diesem Beispiel senden nur Textelemente Signale aus, wenn sie ausgewählt werden; daher müssen wir nicht prüfen, um welche Art von Grafik es sich bei „ item “ handelt.
Wir passen den Zustand der Widgets an die Eigenschaften der Schriftart des ausgewählten Textelements an.
Dies ist der 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."));
}Dieser Slot zeigt im Beispiel ein „Über“-Fenster an, wenn der Benutzer den Menüpunkt „Über“ aus dem Hilfemenü auswählt.
Definition der Klasse „DiagramScene“
Die Klasse „ DiagramScene “ erbt von „ QGraphicsScene “ und erweitert die Funktionalität ihrer Oberklasse um die Verarbeitung von „ DiagramItems “, „ Arrows “ und „ DiagramTextItems “.
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;
};In der ` DiagramScene ` kann ein Mausklick drei verschiedene Aktionen auslösen: Das Element unter dem Mauszeiger kann verschoben werden, ein Element kann eingefügt werden oder ein Pfeil kann zwischen zwei Diagrammelementen verbunden werden. Welche Aktion ein Mausklick auslöst, hängt vom Modus ab, der durch die Enumeration `Mode` festgelegt wird und in dem sich die Szene befindet. Der Modus wird mit der Funktion ` setMode() ` festgelegt.
Die Szene legt außerdem die Farbe ihrer Elemente und die Schriftart ihrer Textelemente fest. Die von der Szene verwendeten Farben und Schriftarten können mit den Funktionen ` setLineColor()`, ` setTextColor()`, ` setItemColor() ` und ` setFont() ` festgelegt werden. Der Typ „ DiagramItem “, der durch die Funktion `DiagramItem::DiagramType` vorgegeben wird und beim Einfügen eines Elements erstellt werden soll, wird über den Slot ` setItemType() ` festgelegt.
Die Klassen „ MainWindow “ und „ DiagramScene “ teilen sich die Verantwortung für die Funktionalität der Beispiele. „ MainWindow “ übernimmt folgende Aufgaben: das Löschen von Elementen, Text und Pfeilen; das Verschieben von Diagrammelementen nach hinten und nach vorne; sowie das Festlegen des Maßstabs der Szene.
Implementierung der Klasse „DiagramScene“
Wir beginnen mit dem Konstruktor:
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;
}Die Szene verwendet ` myItemMenu `, um das Kontextmenü festzulegen, wenn sie ` DiagramItems` erstellt. Wir setzen den Standardmodus auf ` DiagramScene::MoveItem `, da dies das Standardverhalten von ` QGraphicsScene` liefert.
Hier ist die Funktion „ setLineColor() “:
void DiagramScene::setLineColor(const QColor &color)
{
myLineColor = color;
if (isItemChange(Arrow::Type)) {
Arrow *item = qgraphicsitem_cast<Arrow *>(selectedItems().first());
item->setColor(myLineColor);
update();
}
}Die Funktion „ isItemChange “ gibt „true“ zurück, wenn ein Element vom Typ „ Arrow “ in der Szene ausgewählt ist; in diesem Fall möchten wir dessen Farbe ändern. Wenn die Funktion „ DiagramScene “ neue Pfeile erstellt und der Szene hinzufügt, verwendet sie ebenfalls die neue Funktion „ color “.
Hier ist die Funktion „ setTextColor() “:
void DiagramScene::setTextColor(const QColor &color)
{
myTextColor = color;
if (isItemChange(DiagramTextItem::Type)) {
DiagramTextItem *item = qgraphicsitem_cast<DiagramTextItem *>(selectedItems().first());
item->setDefaultTextColor(myTextColor);
}
}Diese Funktion legt die Farbe von „ DiagramTextItems “ genauso fest, wie „ setLineColor() “ die Farbe von „ Arrows “ festlegt.
Hier ist die Funktion „ setItemColor() “:
void DiagramScene::setItemColor(const QColor &color)
{
myItemColor = color;
if (isItemChange(DiagramItem::Type)) {
DiagramItem *item = qgraphicsitem_cast<DiagramItem *>(selectedItems().first());
item->setBrush(myItemColor);
}
}Diese Funktion legt die Farbe fest, die die Szene beim Erstellen von „ DiagramItems “ verwendet. Außerdem ändert sie die Farbe eines ausgewählten „ DiagramItem “.
Dies ist die Implementierung von „ 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);
}
}Legt die Schriftart fest, die für neue und ausgewählte Elemente verwendet werden soll, sofern ein Textelement ausgewählt ist: „ DiagramTextItems “.
Dies ist die Implementierung des Slots „ editorLostFocus() “:
void DiagramScene::editorLostFocus(DiagramTextItem *item)
{
QTextCursor cursor = item->textCursor();
cursor.clearSelection();
item->setTextCursor(cursor);
if (item->toPlainText().isEmpty()) {
removeItem(item);
item->deleteLater();
}
}DiagramTextItems Senden Sie ein Signal aus, wenn der Fokus verloren geht, das mit diesem Slot verbunden ist. Wir entfernen das Element, wenn es keinen Text enthält. Andernfalls würde es zu einem Speicherleck kommen und den Benutzer verwirren, da die Elemente beim Anklicken mit der Maus bearbeitet würden.
Die Funktion „ mousePressEvent() “ verarbeitet Mausklick-Ereignisse unterschiedlich, je nachdem, in welchem Modus sich der „ DiagramScene “ befindet. Wir betrachten die Umsetzung für jeden Modus:
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;Wir erstellen einfach ein neues „ DiagramItem “ und fügen es an der Position, an der die Maus gedrückt wurde, zur Szene hinzu. Beachten Sie, dass der Ursprung seines lokalen Koordinatensystems unter der Position des Mauszeigers liegt.
case InsertLine:
line = new QGraphicsLineItem(QLineF(mouseEvent->scenePos(),
mouseEvent->scenePos()));
line->setPen(QPen(myLineColor, 2));
addItem(line);
break;Der Benutzer fügt „ Arrows “ zur Szene hinzu, indem er eine Linie zwischen den Elementen spannt, die der Pfeil verbinden soll. Der Anfang der Linie wird an der Stelle fixiert, an der der Benutzer mit der Maus geklickt hat, und das Ende folgt dem Mauszeiger, solange die Maustaste gedrückt gehalten wird. Wenn der Benutzer die Maustaste loslässt, wird ein „ Arrow “ zur Szene hinzugefügt, sofern sich unter dem Anfang und dem Ende der Linie ein „ DiagramItem “ befindet. Wie dies umgesetzt wird, sehen wir später; hier fügen wir die Linie einfach hinzu.
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);Der „ DiagramTextItem “ ist bearbeitbar, wenn das Flag „ Qt::TextEditorInteraction “ gesetzt ist, andernfalls lässt er sich mit der Maus verschieben. Wir möchten immer, dass der Text über den anderen Elementen in der Szene dargestellt wird, daher setzen wir den Wert auf eine Zahl, die höher ist als die der anderen Elemente in der Szene.
default:
;
}
QGraphicsScene::mousePressEvent(mouseEvent);
}Befinden wir uns im „MoveItem“-Modus, wenn wir zum Standard-Switch gelangen, können wir die Implementierung von ` QGraphicsScene ` aufrufen, die die Bewegung von Elementen mit der Maus übernimmt. Wir führen diesen Aufruf auch dann durch, wenn wir uns in einem anderen Modus befinden, wodurch es möglich ist, ein Element hinzuzufügen, anschließend die Maustaste gedrückt zu halten und das Element zu verschieben. Bei Textelementen ist dies nicht möglich, da sie keine Mausereignisse weiterleiten, solange sie bearbeitbar sind.
Dies ist die Funktion „ 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);
}
}Wir müssen die Linie zeichnen, wenn wir uns im „InsertMode“ befinden und die Maustaste gedrückt ist (der Wert von „line“ ist ungleich 0). Wie unter „ mousePressEvent() “ erläutert, wird die Linie von der Position, an der die Maustaste gedrückt wurde, bis zur aktuellen Mausposition gezeichnet.
Befinden wir uns im „MoveItem“-Modus, rufen wir die Implementierung von „ QGraphicsScene “ auf, die die Bewegung von Elementen übernimmt.
In der Funktion „ mouseReleaseEvent() “ müssen wir prüfen, ob der Szene ein Pfeil hinzugefügt werden soll:
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;Zunächst müssen wir die Elemente (falls vorhanden) unter den Start- und Endpunkten der Linie abrufen. Die Linie selbst ist das erste Element an diesen Punkten, daher entfernen wir sie aus den Listen. Vorsichtshalber prüfen wir, ob die Listen leer sind, was jedoch niemals vorkommen sollte.
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();
}
}Nun prüfen wir, ob sich unter dem Start- und Endpunkt der Linie zwei verschiedene „ DiagramItems “ befinden. Ist dies der Fall, können wir aus den beiden Elementen ein „ Arrow “ erstellen. Der Pfeil wird dann jedem Element und schließlich der Szene hinzugefügt. Der Pfeil muss aktualisiert werden, um seine Start- und Endpunkte an die Elemente anzupassen. Wir setzen den z-Wert des Pfeils auf -1000,0, da er immer unter den Elementen gezeichnet werden soll.
line = nullptr;
QGraphicsScene::mouseReleaseEvent(mouseEvent);
}Hier ist die Funktion „ 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();
}Die Szene verfügt über eine Einzelauswahl, d. h., es kann jeweils nur ein Element ausgewählt werden. Die „for“-Schleife durchläuft daher einmal das ausgewählte Element oder gar keine, falls kein Element ausgewählt ist. „ isItemChange() “ wird verwendet, um zu prüfen, ob ein ausgewähltes Element vorhanden ist und ob es zum angegebenen Diagramm „ type “ gehört.
Definition der Klasse „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;
};DiagramItem repräsentiert eine Flussdiagramm-Form im DiagramScene. Sie erbt von QGraphicsPolygonItem und verfügt für jede Form über ein Polygon. Die Enumeration DiagramType enthält einen Wert für jede der Flussdiagramm-Formen.
Die Klasse verfügt über eine Liste der mit ihr verbundenen Pfeile. Dies ist notwendig, da nur das Element selbst weiß, wann es (mit der Funktion „ itemChanged() “) verschoben wird – zu diesem Zeitpunkt müssen die Pfeile aktualisiert werden. Das Element kann sich zudem mit der Funktion „ image() “ selbst auf eine „ QPixmap “ zeichnen. Dies wird für die Werkzeugschaltflächen in „ MainWindow “ verwendet, siehe „ createColorToolButtonIcon() “ in „ MainWindow “.
Die Enumeration „Type“ ist eine eindeutige Kennung der Klasse. Sie wird von qgraphicsitem_cast() verwendet, das dynamische Typumwandlungen von Grafikelementen durchführt. Die Konstante „UserType“ ist der Mindestwert, den ein benutzerdefinierter Grafikelementtyp annehmen kann.
Implementierung der Klasse „DiagramItem“
Wir beginnen mit einem Blick auf den Konstruktor:
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);
}Im Konstruktor erstellen wir das Polygon des Elements gemäß diagramType. „ QGraphicsItems“ sind standardmäßig weder verschiebbar noch auswählbar, daher müssen wir diese Eigenschaften festlegen.
Hier ist die Funktion „ removeArrow() “:
void DiagramItem::removeArrow(Arrow *arrow)
{
arrows.removeAll(arrow);
}removeArrow() wird verwendet, um „ Arrow “-Elemente zu entfernen, wenn diese oder die mit ihnen verbundenen „ DiagramItems “ aus der Szene entfernt werden.
Hier ist die Funktion „ 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;
}
}Diese Funktion wird aufgerufen, wenn das Element aus der Szene entfernt wird, und entfernt alle Pfeile, die mit diesem Element verbunden sind. Der Pfeil muss sowohl aus der „ arrows “-Liste des Start- als auch des Endelements entfernt werden. Da entweder das Start- oder das Endelement das Objekt ist, bei dem diese Funktion gerade aufgerufen wird, müssen wir sicherstellen, dass wir mit einer Kopie der Pfeile arbeiten, da „removeArrow()“ diesen Container verändert.
Hier ist die Funktion „ addArrow() “:
void DiagramItem::addArrow(Arrow *arrow)
{
arrows.append(arrow);
}Diese Funktion fügt den „ arrow “ einfach zur „ arrows “-Liste der Elemente hinzu.
Hier ist die Funktion „ 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;
}Diese Funktion zeichnet das Polygon des Elements auf ein ` QPixmap`. In diesem Beispiel verwenden wir dies, um Symbole für die Werkzeugschaltflächen in der Werkzeugleiste zu erstellen.
Hier ist die Funktion „ contextMenuEvent() “:
void DiagramItem::contextMenuEvent(QGraphicsSceneContextMenuEvent *event)
{
scene()->clearSelection();
setSelected(true);
myContextMenu->popup(event->screenPos());
}Wir zeigen das Kontextmenü an. Da Rechtsklicks, die das Menü anzeigen, standardmäßig keine Elemente auswählen, legen wir das ausgewählte Element mit ` setSelected()` fest. Dies ist notwendig, da ein Element ausgewählt sein muss, um seine Höhe mit den Aktionen „ bringToFront “ und „ sendToBack “ zu ändern.
Dies ist die Implementierung von ` itemChange()`:
QVariant DiagramItem::itemChange(GraphicsItemChange change, const QVariant &value)
{
if (change == QGraphicsItem::ItemPositionChange) {
for (Arrow *arrow : std::as_const(arrows))
arrow->updatePosition();
}
return value;
}Wenn sich das Element verschoben hat, müssen wir die Positionen der mit ihm verbundenen Pfeile aktualisieren. Die Implementierung von ` QGraphicsItem ` führt keine Aktion aus, daher geben wir einfach ` value` zurück.
Klassendefinition von `DiagramTextItem`
Die Klasse „ TextDiagramItem “ erbt von „ QGraphicsTextItem “ und bietet zusätzlich die Möglichkeit, bearbeitbare Textelemente zu verschieben. Bearbeitbare „QGraphicsTextItems“ sind so konzipiert, dass sie an ihrer Position fixiert sind und die Bearbeitung beginnt, wenn der Benutzer einmal auf das Element klickt. Bei „ DiagramTextItem “ beginnt die Bearbeitung mit einem Doppelklick, sodass ein einfacher Klick für die Interaktion und das Verschieben des Elements zur Verfügung steht.
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;
};Wir verwenden „ itemChange() “ und „ focusOutEvent() “, um das „ DiagramScene “ zu benachrichtigen, wenn das Textelement den Fokus verliert und ausgewählt wird.
Wir implementieren die Funktionen zur Verarbeitung von Mausereignissen neu, um das Mausverhalten von „ QGraphicsTextItem “ anpassen zu können.
Implementierung von `DiagramTextItem`
Wir beginnen mit dem Konstruktor:
DiagramTextItem::DiagramTextItem(QGraphicsItem *parent)
: QGraphicsTextItem(parent)
{
setFlag(QGraphicsItem::ItemIsMovable);
setFlag(QGraphicsItem::ItemIsSelectable);
}Wir setzen das Element einfach auf „bewegbar“ und „auswählbar“, da diese Flags standardmäßig deaktiviert sind.
Hier ist die Funktion ` itemChange() `:
QVariant DiagramTextItem::itemChange(GraphicsItemChange change,
const QVariant &value)
{
if (change == QGraphicsItem::ItemSelectedHasChanged)
emit selectedChange(this);
return value;
}Wenn das Element ausgewählt wird, senden wir das Signal `selectedChanged`. Das ` MainWindow ` nutzt dieses Signal, um die Widgets, die Schriftarteneigenschaften anzeigen, auf die Schriftart des ausgewählten Textelements zu aktualisieren.
Hier ist die Funktion „ focusOutEvent() “:
void DiagramTextItem::focusOutEvent(QFocusEvent *event)
{
setTextInteractionFlags(Qt::NoTextInteraction);
emit lostFocus(this);
QGraphicsTextItem::focusOutEvent(event);
}DiagramScene nutzt das Signal, das ausgelöst wird, wenn das Textelement den Fokus verliert, um das Element zu entfernen, sofern es leer ist, d. h. keinen Text enthält.
Dies ist die Implementierung von ` mouseDoubleClickEvent()`:
void DiagramTextItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
{
if (textInteractionFlags() == Qt::NoTextInteraction)
setTextInteractionFlags(Qt::TextEditorInteraction);
QGraphicsTextItem::mouseDoubleClickEvent(event);
}Wenn wir ein Doppelklick-Ereignis empfangen, machen wir das Element durch den Aufruf von ` QGraphicsTextItem::setTextInteractionFlags()` bearbeitbar. Anschließend leiten wir den Doppelklick an das Element selbst weiter.
Definition der Klasse „Arrow“
Die Klasse „ Arrow “ ist ein Grafikobjekt, das zwei „ DiagramItems “-Elemente miteinander verbindet. Sie zeichnet eine Pfeilspitze an einem der Elemente. Dazu muss das Element sich selbst zeichnen und außerdem Methoden neu implementieren, die von der Grafikszene zur Kollisions- und Auswahlprüfung verwendet werden. Die Klasse erbt von der Klasse „QGraphicsLine“ und zeichnet die Pfeilspitze sowie bewegt sich mit den Elementen, die sie verbindet.
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;
};Die Farbe des Elements kann mit ` setColor()` festgelegt werden.
boundingRect() sowie „ shape() “ werden von „ QGraphicsLineItem “ neu implementiert und von der Szene zur Überprüfung von Kollisionen und Auswahlen verwendet.
Der Aufruf von ` updatePosition() ` bewirkt, dass der Pfeil seine Position und den Winkel der Pfeilspitze neu berechnet. ` paint() ` wurde neu implementiert, damit wir einen Pfeil anstelle einer einfachen Linie zwischen den Elementen zeichnen können.
myStartItem und myEndItem sind die Diagrammelemente, die der Pfeil verbindet. Der Pfeil wird so gezeichnet, dass seine Spitze auf das Endelement zeigt. arrowHead ist ein Polygon mit drei Eckpunkten, das wir zum Zeichnen der Pfeilspitze verwenden.
Implementierung der Klasse „Arrow“
Der Konstruktor der Klasse ` Arrow ` sieht wie folgt aus:
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));
}Wir legen das Start- und Enddiagrammelement des Pfeils fest. Die Pfeilspitze wird an der Stelle gezeichnet, an der die Linie das Endelement schneidet.
Hier ist die Funktion „ 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);
}Wir müssen diese Funktion neu implementieren, da der Pfeil größer ist als das Begrenzungsrechteck der Klasse „ QGraphicsLineItem “. Die Grafikszene verwendet das Begrenzungsrechteck, um zu ermitteln, welche Bereiche der Szene aktualisiert werden müssen.
Hier ist die Funktion „ shape() “:
QPainterPath Arrow::shape() const
{
QPainterPath path = QGraphicsLineItem::shape();
path.addPolygon(arrowHead);
return path;
}Die Funktion „shape“ gibt ein „ QPainterPath “ zurück, das genau die Form des Elements hat. Die Funktion „ QGraphicsLineItem::shape()“ gibt einen Pfad zurück, auf dem mit dem aktuellen Stift eine Linie gezeichnet wird, sodass wir nur noch die Pfeilspitze hinzufügen müssen. Diese Funktion wird verwendet, um mit der Maus auf Kollisionen und Auswahlen zu prüfen.
Hier ist der Slot „ updatePosition() “:
void Arrow::updatePosition()
{
QLineF line(mapFromItem(myStartItem, 0, 0), mapFromItem(myEndItem, 0, 0));
setLine(line);
}Dieser Slot aktualisiert den Pfeil, indem er den Start- und Endpunkt seiner Linie auf die Mitte der Elemente setzt, die er verbindet.
Hier ist die Funktion „ 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);Wenn das Start- und das Endelement kollidieren, zeichnen wir den Pfeil nicht; der Algorithmus, mit dem wir den Punkt ermitteln, an dem der Pfeil gezeichnet werden soll, kann fehlschlagen, wenn die Elemente kollidieren.
Zunächst legen wir den Stift und den Pinsel fest, die wir zum Zeichnen des Pfeils verwenden werden.
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()));Anschließend müssen wir die Position ermitteln, an der die Pfeilspitze gezeichnet werden soll. Die Spitze sollte dort gezeichnet werden, wo sich die Linie und das Endelement schneiden. Dazu wird die Linie zwischen jedem Punkt des Polygons ermittelt und geprüft, ob sie die Linie des Pfeils schneidet. Da der Start- und Endpunkt der Linie auf die Mitte der Objekte gesetzt sind, sollte die Pfeillinie genau eine der Linien des Polygons schneiden. Beachte, dass die Punkte im Polygon relativ zum lokalen Koordinatensystem des Objekts sind. Wir müssen daher die Position des Endobjekts addieren, um die Koordinaten relativ zur Szene zu machen.
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;Wir berechnen den Winkel zwischen der x-Achse und der Pfeillinie. Wir müssen die Pfeilspitze um diesen Winkel drehen, damit sie der Richtung des Pfeils folgt. Ist der Winkel negativ, müssen wir die Richtung des Pfeils umkehren.
Anschließend können wir die drei Punkte des Pfeilspitzenpolygons berechnen. Einer der Punkte ist das Ende der Linie, das nun den Schnittpunkt zwischen der Pfeillinie und dem Endpolygon darstellt. Dann löschen wir das Polygon „ arrowHead “ der zuvor berechneten Pfeilspitze und setzen diese neuen Punkte.
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);
}
}Wenn die Linie ausgewählt ist, zeichnen wir zwei gepunktete Linien, die parallel zur Pfeillinie verlaufen. Wir verwenden nicht die Standardimplementierung, die ` boundingRect()` nutzt, da das Begrenzungsrechteck von ` QRect ` deutlich größer ist als die Linie.
© 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.