Beispiel für eine Diagrammszene

Demonstration der Verwendung des Graphics View Frameworks.

Das Diagrammszenen-Beispiel ist eine Anwendung, in der Sie ein Flussdiagramm erstellen können. Es ist möglich, Flussdiagrammformen und Text hinzuzufügen und die Formen durch Pfeile zu verbinden, wie im Bild oben gezeigt. Die Formen, Pfeile und der Text können mit verschiedenen Farben versehen werden, und es ist möglich, die Schriftart, den Stil und die Unterstreichung des Textes zu ändern.

Das Qt-Grafik-Ansichts-Framework wurde entwickelt, um benutzerdefinierte 2D-Grafikelemente zu verwalten und anzuzeigen. Die Hauptklassen des Frameworks sind QGraphicsItem, QGraphicsScene und QGraphicsView. Die Grafikszene verwaltet die Elemente und bietet eine Oberfläche für sie. QGraphicsView ist ein Widget, das zum Rendern einer Szene auf dem Bildschirm verwendet wird. Eine genauere Beschreibung des Frameworks finden Sie im Graphics View Framework.

In diesem Beispiel wird gezeigt, wie man solche benutzerdefinierten Grafikszenen 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 Objekten zu behandeln.
  • Implementierung einer Grafikszene, die unsere benutzerdefinierten Elemente verwalten kann.
  • Benutzerdefiniertes Malen von Objekten.
  • Erstellen eines beweglichen und editierbaren Textelements.

Das Beispiel besteht aus den folgenden Klassen:

  • MainWindow erstellt die Widgets und zeigt sie in einer QMainWindow an. Sie verwaltet auch die Interaktion zwischen den Widgets und der Grafikszene, der Ansicht und den Elementen.
  • DiagramItem erbt QGraphicsPolygonItem und stellt eine Flussdiagrammform dar.
  • TextDiagramItem erbt QGraphicsTextItem und stellt Textelemente im Diagramm dar. Die Klasse unterstützt das Verschieben des Elements mit der Maus, was von QGraphicsTextItem nicht unterstützt wird.
  • Arrow erbt QGraphicsLineItem und ist ein Pfeil, der zwei Diagrammelemente miteinander verbindet.
  • DiagramScene erbt QGraphicsDiagramScene und bietet Unterstützung für DiagramItem, Arrow und DiagramTextItem (zusätzlich zu der bereits von QGraphicsScene geleisteten Unterstützung).

Definition der MainWindow-Klasse

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 und platziert die Widgets in einer QMainWindow. Die Klasse leitet die Eingaben der Widgets an die DiagramScene weiter. Sie aktualisiert auch 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 auch Elemente aus der Szene und verwaltet die z-Reihenfolge, die entscheidet, in welcher Reihenfolge die 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 erstellen. Die Symbolleisten müssen nach der Szene erstellt werden, da sie mit deren Signalen verbunden sind. Anschließend legen wir die Widgets im Fenster aus.

Wir stellen eine Verbindung zu den Slots itemInserted() und textInserted() der Diagrammszenen her, da wir die Markierung der Schaltflächen im Werkzeugkasten aufheben wollen, wenn ein Element eingefügt wird. Wenn ein Element in der Szene ausgewählt wird, erhalten wir das Signal itemSelected(). Wir verwenden dieses Signal, um die Widgets zu aktualisieren, die die Schrifteigenschaften anzeigen, wenn das ausgewählte Element ein DiagramTextItem ist.

Die Funktion createToolBox() erstellt und legt die Widgets der toolBox QToolBox an. Wir werden diese Funktion nicht im Detail untersuchen, da sie keine spezifischen Funktionen des Grafik-Frameworks behandelt. 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 Registerkarten-Widget ein, das die Formen des Flussdiagramms enthält. Bei einer exklusiven QButtonGroup ist immer nur eine Schaltfläche aktiviert; wir möchten, dass die Gruppe es ermöglicht, alle Schaltflächen zu deaktivieren. Wir verwenden dennoch eine Schaltflächengruppe, da wir jeder Schaltfläche Benutzerdaten zuordnen können, die wir zum Speichern des Diagrammtyps verwenden. Die Funktion createCellWidget() richtet die Schaltflächen im Element des Registerkarten-Widgets ein und wird später untersucht.

Die Schaltflächen des Registerkarten-Widgets im Hintergrund werden auf die gleiche Weise eingerichtet, so dass wir die Erstellung des Werkzeugkastens überspringen:

    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 des Werkzeugkastens als Maximum fest. Auf diese Weise wird mehr Platz für die Grafikansicht geschaffen.

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 besprochen, mit denen wir die Aktionen verbinden.

Das 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 QToolButtonin colorToolBar, fontColorToolButton, fillColorToolButton und lineColorToolButton sind interessant, da wir für sie Icons erstellen, indem wir auf QPixmap mit QPainter zeichnen. Wir zeigen, wie die 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 stellen das Menü der Werkzeugschaltfläche mit setMenu() ein. Das Objekt fillAction QAction muss immer auf die ausgewählte Aktion des Menüs zeigen. 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 haben können. Wenn der Benutzer auf die Schaltfläche drückt, die das Signal clicked() auslöst, 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 Schaltfläche und eine Beschriftung enthalten. Die mit dieser Funktion erstellten Widgets werden für das Hintergrund-Widget in der Toolbox 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 von einem der DiagramItems enthält, d.h. Flussdiagrammformen. Das Bild wird von DiagramItem mit Hilfe der Funktion image() erstellt. Die Klasse QButtonGroup ermöglicht es uns, jeder Schaltfläche eine ID (int) zuzuordnen; wir speichern den Typ des Diagramms, d.h. das DiagramItem::DiagramType enum. Wir verwenden den gespeicherten Diagrammtyp, wenn wir neue Diagrammelemente für die Szene erstellen. Die mit dieser Funktion erstellten Widgets werden in der Toolbox 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 in colorToolBar verwendet wird. Für jede Farbe, die wir dem Menü hinzufügen, erstellen wir 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 wird verwendet, um die QIcon der fillColorToolButton, fontColorToolButton und lineColorToolButton zu erstellen. Der String imageFile ist entweder der Text, die Füllung oder das Liniensymbol, das für die Schaltflächen verwendet wird. Unterhalb des Bildes zeichnen wir mit 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 für die Erstellung von Symbolen für die Farbmenüs in fillColorToolButton, fontColorToolButton und lineColorToolButton verwendet.

Hier ist der backgroundButtonGroupClicked() Slot:

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 die QBrush fest, die verwendet wird, um den Hintergrund der Diagrammszene zu zeichnen. Der Hintergrund kann ein Gitter aus blauen, grauen oder weißen Kacheln sein, oder auch gar kein Gitter. Wir haben QPixmaps der Kacheln aus png-Dateien, mit denen wir den Pinsel erstellen.

Wenn eine der Schaltflächen im Hintergrund des Registerkarten-Widgets angeklickt wird, ändern wir den Pinsel; wir finden heraus, welche Schaltfläche es ist, indem wir ihren 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 markiert ist. Wenn eine Schaltfläche markiert ist, kann der Benutzer auf die Grafikansicht klicken und eine DiagramItem des ausgewählten Typs wird in DiagramScene eingefügt. Wir müssen die Schaltflächen in der Gruppe in einer Schleife durchlaufen, um die Markierung anderer Schaltflächen aufzuheben, da immer nur eine Schaltfläche markiert sein darf.

QButtonGroup weist jeder Schaltfläche eine ID zu. Wir haben die id jeder Schaltfläche auf den Diagrammtyp gesetzt, wie er durch DiagramItem::DiagramType angegeben ist, der in die Szene eingefügt wird, wenn er angeklickt wird. Wir können dann die id der Schaltfläche verwenden, wenn wir den Diagrammtyp mit setItemType() festlegen. Im Fall von Text haben wir eine id zugewiesen, die einen Wert hat, der nicht im DiagramType-Enum 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. Er löscht zuerst die Pfeile, um zu vermeiden, dass sie zweimal gelöscht werden. Wenn das zu löschende Element ein DiagramItem ist, müssen wir auch die mit ihm verbundenen Pfeile löschen; wir wollen keine Pfeile in der Szene, die nicht mit Elementen an beiden Enden 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 ItemMove- oder InsertLine-Modus befindet. Diese Schaltflächengruppe ist exklusiv, d.h. es wird immer nur eine Schaltfläche geprüft. Wie bei buttonGroup oben haben wir den Schaltflächen eine ID zugewiesen, die mit den Werten des DiagramScene::Mode enum übereinstimmt, so dass wir die ID verwenden können, um den richtigen Modus einzustellen.

Hier ist der bringToFront() Slot:

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);
}

In der Szene können mehrere Elemente miteinander kollidieren, d.h. sich überlappen. Dieser Slot wird aufgerufen, wenn der Benutzer anfordert, dass ein Element über den Elementen platziert werden soll, mit denen es kollidiert. QGrapicsItems hat einen z-Wert, der die Reihenfolge bestimmt, in der die Elemente in der Szene gestapelt werden; man kann ihn sich wie die z-Achse in einem 3D-Koordinatensystem vorstellen. Wenn Elemente kollidieren, werden die Elemente mit höheren z-Werten über die Elemente mit niedrigeren Werten gezeichnet. Wenn wir ein Element in den Vordergrund bringen, können wir die Elemente, mit denen es kollidiert, in einer Schleife durchgehen und einen z-Wert festlegen, der höher ist als alle anderen.

Hier ist der sendToBack() Slot:

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 auf die gleiche Weise wie der oben beschriebene bringToFront(), setzt aber einen z-Wert, der niedriger ist als die Gegenstände, mit denen der Gegenstand, der nach hinten geschickt werden soll, kollidiert.

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 auf den Modus zurück, bevor das Element eingefügt wurde, d.h. ItemMove oder InsertText, je nachdem, welche Schaltfläche in der pointerTypeGroup aktiviert ist. Wir müssen auch die Schaltfläche 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 einfach den Modus der Szene auf den Modus zurück, den sie hatte, bevor der Text eingefügt wurde.

Hier ist der currentFontChanged() Slot:

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

Wenn der Benutzer eine Änderung der Schriftart anfordert, indem er eines der Widgets in fontToolBar verwendet, erstellen wir ein neues QFont Objekt und setzen seine Eigenschaften so, dass sie dem Zustand der Widgets entsprechen. Dies geschieht in handleFontChange(), also rufen wir einfach diesen Slot auf.

Hier ist der fontSizeChanged() Slot:

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

Wenn der Benutzer eine Schriftartänderung anfordert, indem er eines der Widgets in fontToolBar verwendet, erstellen wir ein neues QFont Objekt und stellen seine Eigenschaften so ein, dass sie mit dem Zustand der Widgets übereinstimmen. Dies geschieht in handleFontChange(), also 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 mit sceneScaleCombo den Maßstab erhöhen oder verringern, in dem die Szene gezeichnet wird. Es ist nicht die Szene selbst, die ihren Maßstab ändert, sondern nur die Ansicht.

Hier ist der textColorChanged() Slot:

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 Element im Dropdown-Menü von fontColorToolButton gedrückt wird. Wir müssen das Symbol auf der Schaltfläche in die Farbe des ausgewählten QAction ändern. Wir behalten einen Zeiger auf die ausgewählte Aktion in textAction. In textButtonTriggered() ändern wir die Textfarbe in die Farbe von textAction, also 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 behandelt Anfragen zur Änderung der Farbe von DiagramItems auf die gleiche Weise wie textColorChanged() für DiagramTextItems.

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 behandelt Anfragen zum Ändern der Farbe von Arrows auf die gleiche Weise wie textColorChanged() für DiagramTextItems.

Hier ist der textButtonTriggered() Slot:

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

textAction Zeigt auf die QAction des aktuell ausgewählten Menüpunkts im Farb-Dropdown-Menü von fontColorToolButton. Wir haben die Daten der Aktion auf die QColor gesetzt, die die Aktion darstellt, so dass wir diese einfach abrufen können, wenn wir die Farbe des Textes mit setTextColor() setzen.

Hier ist der fillButtonTriggered() Slot:

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

fillAction zeigt auf den ausgewählten Menüpunkt im Dropdown-Menü von fillColorToolButton(). Wir können also die Daten dieser Aktion verwenden, wenn wir die Farbe des Eintrags mit setItemColor() einstellen.

Hier ist der Slot lineButtonTriggered():

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

lineAction zeigt auf den ausgewählten Eintrag im Dropdown-Menü von lineColorToolButton. Wir verwenden seine Daten, wenn wir die Pfeilfarbe mit setLineColor() einstellen.

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, die Schrifteigenschaften anzeigen, ändert. Wir erstellen ein neues QFont Objekt und legen seine Eigenschaften auf der Grundlage der Widgets fest. Anschließend rufen wir die Funktion setFont() von DiagramScene auf; es ist die Szene, die die Schriftart des von ihr verwalteten DiagramTextItems einstellt.

Hier ist der itemSelected() Slot:

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 in DiagramScene ausgewählt wird. Im Fall dieses Beispiels sind es nur Textelemente, die Signale aussenden, wenn sie ausgewählt werden, daher brauchen wir nicht zu prüfen, um welche Art von Grafik es sich bei item handelt.

Wir stellen den Zustand der Widgets so ein, dass er mit den Eigenschaften der Schriftart des ausgewählten Textelements übereinstimmt.

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 eine About-Box für das Beispiel an, wenn der Benutzer den Menüpunkt About aus dem Hilfemenü auswählt.

DiagramScene-Klassendefinition

Die Klasse DiagramScene erbt von QGraphicsScene und fügt zusätzlich zu den Elementen, die von ihrer Oberklasse behandelt werden, Funktionen für DiagramItems, Arrows und DiagramTextItems hinzu.

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 DiagramScene kann ein Mausklick drei verschiedene Aktionen auslösen: Das Element unter der Maus 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 von dem Modus ab, der durch das Mode-Enum angegeben wird und in dem sich die Szene befindet. Der Modus wird mit der Funktion setMode() eingestellt.

Die Szene legt auch 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() eingestellt werden. Der Typ von DiagramItem, gegeben durch die Funktion DiagramItem::DiagramType, der beim Einfügen eines Elements erstellt wird, wird mit dem Slot setItemType() festgelegt.

MainWindow und DiagramScene teilen sich die Verantwortung für die Funktionalität der Beispiele. MainWindow übernimmt die folgenden Aufgaben: das Löschen von Elementen, Text und Pfeilen, das Verschieben von Diagrammelementen nach hinten und vorne und das Einstellen 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ü zu setzen, wenn sie DiagramItems erstellt. Wir setzen den Standardmodus auf DiagramScene::MoveItem, da dies das Standardverhalten von QGraphicsScene ergibt.

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 Arrow Element in der Szene ausgewählt ist, dessen Farbe wir ändern wollen. Wenn DiagramScene neue Pfeile erstellt und zur Szene hinzufügt, wird auch die neue color verwendet.

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 stellt die Farbe von DiagramTextItems genauso ein, wie setLineColor() die Farbe von Arrows.

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. Sie ändert auch 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);
    }
}

Legen Sie die Schriftart fest, die für neue und ausgewählte Textelemente verwendet werden soll, DiagramTextItems.

Dies ist die Implementierung von editorLostFocus() slot:

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

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

DiagramTextItems Wir geben ein Signal aus, wenn sie den Fokus verlieren, das mit diesem Slot verbunden ist. Wir entfernen das Element, wenn es keinen Text hat. Andernfalls würden wir Speicherplatz verlieren und den Benutzer verwirren, da die Elemente bearbeitet werden, wenn sie mit der Maus angeklickt werden.

Die Funktion mousePressEvent() behandelt die Mausdruckereignisse unterschiedlich, je nachdem, in welchem Modus sich DiagramScene befindet. Wir untersuchen ihre Implementierung 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 der Szene an der Position hinzu, an der die Maus gedrückt wurde. 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 in die Szene ein, indem er eine Linie zwischen den Elementen zieht, 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 Taste gedrückt gehalten wird. Wenn der Benutzer die Maustaste loslässt, wird der Szene ein Arrow hinzugefügt, wenn sich unter dem Anfang und dem Ende der Linie ein DiagramItem befindet. Wir werden später sehen, wie dies umgesetzt wird; hier fügen wir einfach die Zeile 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);

Die DiagramTextItem ist editierbar, wenn das Qt::TextEditorInteraction Flag gesetzt ist, ansonsten ist sie mit der Maus verschiebbar. Wir wollen, dass der Text immer über den anderen Elementen in der Szene gezeichnet wird, also setzen wir den Wert auf eine höhere Zahl als die anderen Elemente in der Szene.

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

Wir befinden uns im MoveItem-Modus, wenn wir den Standardschalter erreichen; dann können wir die Implementierung QGraphicsScene aufrufen, die die Bewegung von Elementen mit der Maus handhabt. Dieser Aufruf erfolgt auch, wenn wir uns in einem anderen Modus befinden, so dass es möglich ist, ein Element hinzuzufügen und dann die Maustaste gedrückt zu halten und das Element zu bewegen. Bei Textelementen ist dies nicht möglich, da sie keine Mausereignisse weitergeben, wenn sie editierbar 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 (die Linie ist nicht 0). Wie in mousePressEvent() beschrieben, wird die Linie von der Position, an der die Maus gedrückt wurde, bis zur aktuellen Position der Maus gezeichnet.

Wenn wir uns im MoveItem-Modus befinden, rufen wir die QGraphicsScene -Implementierung auf, die das Verschieben von Elementen behandelt.

In der Funktion mouseReleaseEvent() müssen wir prüfen, ob ein Pfeil zur Szene 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 Anfangs- und Endpunkten der Zeile ermitteln. Die Zeile selbst ist das erste Element an diesen Punkten, also entfernen wir sie aus den Listen. Als Vorsichtsmaßnahme prüfen wir, ob die Listen leer sind, aber das sollte nie passieren.

        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 den Anfangs- und Endpunkten der Zeilen zwei verschiedene DiagramItems befinden. Wenn ja, können wir ein Arrow mit den beiden Elementen erstellen. Der Pfeil wird dann zu jedem Element und schließlich zur 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 wir wollen, dass er immer unter den Elementen gezeichnet wird.

    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 hat eine Einfachauswahl, d.h. es kann immer nur ein Element ausgewählt werden. Die for-Schleife durchläuft dann eine Schleife mit dem ausgewählten Element oder mit keinem, wenn kein Element ausgewählt ist. isItemChange() wird verwendet, um zu prüfen, ob ein ausgewähltes Element vorhanden ist und auch zu dem angegebenen Diagramm type gehört.

DiagramItem Klassendefinition

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;
};

Die Klasse DiagramItem repräsentiert eine Flussdiagrammform in DiagramScene. Sie erbt QGraphicsPolygonItem und hat ein Polygon für jede Form. Das enum DiagramType hat einen Wert für jede der Flussdiagrammformen.

Die Klasse hat eine Liste der Pfeile, die mit ihr verbunden sind. Dies ist notwendig, da nur das Element weiß, wann es verschoben wird (mit der Funktion itemChanged() ) und zu welchem Zeitpunkt die Pfeile aktualisiert werden müssen. Mit der Funktion image() kann das Element auch selbst auf eine QPixmap gezeichnet werden. Dies wird für die Werkzeugschaltflächen in MainWindow verwendet, siehe createColorToolButtonIcon() in MainWindow.

Das Type enum ist ein eindeutiger Bezeichner der Klasse. Es wird von qgraphicsitem_cast() verwendet, das dynamische Casts von Grafikelementen durchführt. Die Konstante UserType ist der Mindestwert, den ein benutzerdefinierter Grafikelementtyp haben 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. QGraphicsItemist standardmäßig nicht verschiebbar oder auswählbar, daher müssen wir diese Eigenschaften einstellen.

Hier ist die Funktion removeArrow():

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

removeArrow() wird verwendet, um Arrow Elemente zu entfernen, wenn sie oder DiagramItems mit denen sie verbunden sind, 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 Endpunktes entfernt werden. Da entweder das Start- oder das Endelement das Objekt ist, in 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 einfach das Element arrow zur Liste arrows 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 QPixmap. In diesem Beispiel verwenden wir dies, um Symbole für die Werkzeugschaltflächen im Werkzeugkasten 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 rechte Mausklicks, die das Menü anzeigen, standardmäßig keine Elemente auswählen, setzen wir das ausgewählte Element mit setSelected(). Dies ist notwendig, da ein Element ausgewählt werden 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 das Element verschoben wurde, müssen wir die Positionen der mit ihm verbundenen Pfeile aktualisieren. Die Implementierung von QGraphicsItem tut nichts, also geben wir einfach value zurück.

DiagramTextItem Klassendefinition

Die Klasse TextDiagramItem erbt von QGraphicsTextItem und fügt die Möglichkeit hinzu, editierbare Textelemente zu verschieben. Editierbare QGraphicsTextItems sind so konzipiert, dass sie an einer Stelle fixiert sind und die Bearbeitung beginnt, wenn der Benutzer auf das Element klickt. Mit DiagramTextItem beginnt die Bearbeitung mit einem Doppelklick, so dass ein einfacher Klick zur Verfügung steht, um damit zu interagieren und es zu verschieben.

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 DiagramScene zu benachrichtigen, wenn das Textelement den Fokus verliert und ausgewählt wird.

Wir reimplementieren die Funktionen, die Maus-Ereignisse behandeln, um es möglich zu machen, das Mausverhalten von QGraphicsTextItem zu ändern.

DiagramTextItem Implementierung

Wir beginnen mit dem Konstruktor:

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

Wir setzen einfach das Element movable und selectable, da diese Flags standardmäßig ausgeschaltet 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, geben wir das Signal selectedChanged aus. MainWindow verwendet dieses Signal, um die Widgets, die Schrifteigenschaften 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 verwendet das Signal, das ausgegeben wird, wenn das Textelement den Fokus verliert, um das Element zu entfernen, wenn 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 erhalten, machen wir das Element editierbar, indem wir QGraphicsTextItem::setTextInteractionFlags() aufrufen. Anschließend leiten wir den Doppelklick an das Element selbst weiter.

Definition der Pfeilklasse

Die Klasse Arrow ist ein grafisches Element, das zwei DiagramItems miteinander verbindet. Sie zeichnet eine Pfeilspitze auf eines der Elemente. Um dies zu erreichen, muss das Element sich selbst malen und auch Methoden neu implementieren, die von der Grafikszene verwendet werden, um Kollisionen und Auswahlen zu überprüfen. Die Klasse erbt QGraphicsLine item, zeichnet die Pfeilspitze und 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 Objekts kann mit setColor() eingestellt werden.

boundingRect() und shape() wurden von QGraphicsLineItem neu implementiert und werden von der Szene verwendet, um Kollisionen und Auswahlen zu überprüfen.

Der Aufruf von updatePosition() veranlasst den Pfeil, seine Position und den Winkel der Pfeilspitze neu zu berechnen. paint() wurde neu implementiert, damit wir einen Pfeil und nicht nur eine Linie zwischen Elementen malen können.

myStartItem und myEndItem sind die Diagrammelemente, die der Pfeil miteinander verbindet. Der Pfeil wird mit der Spitze zum Endelement gezeichnet. arrowHead ist ein Polygon mit drei Scheitelpunkten, das zum Zeichnen der Pfeilspitze verwendet wird.

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 die Start- und Endpunkte des Pfeils fest. Die Pfeilspitze wird dort gezeichnet, wo sich die Linie mit dem Endpunkt 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, weil der Pfeil größer ist als das Begrenzungsrechteck der QGraphicsLineItem. Die Grafikszene verwendet das Begrenzungsrechteck, um zu wissen, 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 "Form" gibt eine QPainterPath zurück, die genau der Form des Elements entspricht. Die Funktion QGraphicsLineItem::shape() gibt einen Pfad mit einer Linie zurück, die mit dem aktuellen Stift gezeichnet wurde, so dass wir nur die Pfeilspitze hinzufügen müssen. Diese Funktion wird verwendet, um auf Kollisionen und Auswahlen mit der Maus zu prüfen.

Hier ist der updatePosition() Slot:

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

Dieser Slot aktualisiert den Pfeil, indem er die Start- und Endpunkte 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 die Start- und Endpunkte kollidieren, wird der Pfeil nicht gezeichnet; der Algorithmus, den wir verwenden, um den Punkt zu finden, an dem der Pfeil gezeichnet werden soll, kann fehlschlagen, wenn die Elemente kollidieren.

Zunächst legen wir den Stift und den Pinsel fest, mit denen wir den Pfeil zeichnen wollen.

    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()));

Dann müssen wir die Position finden, an der die Pfeilspitze gezeichnet werden soll. Die Spitze sollte dort gezeichnet werden, wo sich die Linie und das Endelement schneiden. Dazu nehmen wir die Linie zwischen den einzelnen Punkten des Polygons und prüfen, ob sie sich mit der Linie des Pfeils schneidet. Da die Anfangs- und Endpunkte der Linie auf den Mittelpunkt der Elemente festgelegt sind, sollte die Pfeillinie nur eine der Linien des Polygons schneiden. Beachten Sie, dass sich die Punkte im Polygon auf das lokale Koordinatensystem des Elements beziehen. Wir müssen daher die Position des Endpunktes hinzufügen, um die Koordinaten relativ zur Szene zu setzen.

    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 Linie des Pfeils. Die Pfeilspitze muss um diesen Winkel gedreht werden, damit sie der Richtung des Pfeils folgt. Wenn der Winkel negativ ist, müssen wir die Pfeilrichtung umkehren.

Dann können wir die drei Punkte des Polygons der Pfeilspitze berechnen. Einer der Punkte ist das Ende der Linie, das jetzt der Schnittpunkt zwischen der Pfeillinie und dem Endpolygon ist. Dann löschen wir das arrowHead Polygon von 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() verwendet, da das QRect Begrenzungsrechteck wesentlich größer ist als die Linie.

Beispielprojekt @ code.qt.io

© 2025 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.