场景图例示例

演示如何使用图形视图框架。

“场景图例”示例是一个应用程序,您可以在其中创建流程图。可以根据上图所示添加流程图形状和文本,并通过箭头连接这些形状。这些形状、箭头和文本可以赋予不同的颜色,还可以更改文本的字体、样式和下划线。

Qt 图形视图框架旨在管理并显示自定义 2D 图形项。框架的主要类是 QGraphicsItemQGraphicsSceneQGraphicsView。图形场景管理项并提供它们的一个表面。 QGraphicsView 是一个小部件,用于在屏幕上渲染场景。请参阅 图形视图框架 以获取关于框架的更详细描述。

在这个示例中,我们通过实现继承自 QGraphicsSceneQGraphicsItem 的类来展示如何创建这样的自定义图形场景和项。

具体来说,我们展示了以下内容:

  • 创建自定义图形项。
  • 处理鼠标事件和项的移动。
  • 实现一个可以管理我们的自定义项的图形场景。
  • 对项进行自定义绘画。
  • 创建一个可移动和可编辑的文本项。

此示例由以下类组成

  • MainWindow 创建小部件并将它们显示在 QMainWindow 中。它还管理小部件和图形场景、视图和项之间的交互。它还在小部件遇到流程图场景的文本项更改时更新其小部件,或在场景中插入流程图项或流程图文本项时更新其小部件。
  • DiagramItem 继承自 QGraphicsPolygonItem,并代表流程图形状。
  • TextDiagramItem 继承自 QGraphicsTextItem,并在图中表示文本项。该类增加了对使用鼠标移动项的支持,这是 QGraphicsTextItem 所未支持的。
  • Arrow 继承自 QGraphicsLineItem,是连接两个流程图项的箭头。
  • DiagramScene 继承自 QGraphicsDiagramScene,并提供了对 `DiagramItem`、`Arrow` 和 `DiagramTextItem` 的支持(除 QGraphicsScene 已处理的之外)。

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

MainWindow 类在 QMainWindow 中创建和布局小部件。该类将小部件的输入转发到 DiagramScene。当流程图场景的文本项更改,或向场景中插入流程图项或流程图文本项时,它还更新其小部件。

这个类还负责从场景中删除项目并处理Z排序,这决定了项目重叠时的绘制顺序。

MainWindow 类实现

我们先看看构造函数

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

在构造函数中,我们在创建图形场景之前调用方法来创建示例的小部件和布局。工具栏必须在场景之后创建,因为它们连接到其信号。然后我们在窗口中布置小部件。

我们连接到图形场景的 itemInserted()textInserted() 槽,因为我们希望在项目插入时要取消选中工具箱中的按钮。当场景中选择项目时,我们收到 itemSelected() 信号。如果我们选择的项目是 DiagramTextItem,我们使用这个信号来更新显示字体属性的控件。

createToolBox() 函数创建并布置 toolbox QToolBox 中的小部件。由于它不涉及图形框架特定的功能,所以我们不会详细讨论它。以下是它的实现:

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

此函数部分设置包含流程图形状的选项卡小部件。一个排它性 QButtonGroup 始终保持一个按钮被选中;我们希望这个组允许所有按钮都不被选中。我们仍然使用按钮组,因为我们可以将用户数据与每个按钮关联起来,这些数据用于存储图形类型。稍后将讨论到 createCellWidget() 函数。

背景选项卡小部件的按钮也以同样的方式设置,因此我们跳到工具箱的创建。

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

我们将工具箱的最佳大小设置为它的最大值。这样,更多空间留给了图形视图。

以下是 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);

这里展示了创建动作的例子。动作触发的功能将在我们连接动作的槽中讨论。

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

我们创建了示例中的三个菜单。

createToolbars() 函数设置了示例的工具栏。在 colorToolBarfontColorToolButtonfillColorToolButtonlineColorToolButton 中有三个 QToolButton 是有趣的,因为我们通过使用 QPixmapQPainter 绘制图标。我们展示了 fillColorToolButton 的创建。此按钮允许用户选择用于图形项目的颜色。

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

我们使用 setMenufillAction QAction 对象。菜单通过 createColorMenu() 函数创建,正如我们稍后看到的,每个项目可以有的颜色都有一个菜单项。当用户按下按钮(这将触发 clicked() 信号)时,我们可以将选中项目的颜色设置为 fillAction 的颜色。我们通过 createColorToolButtonIcon() 创建该按钮的图标。

    ...
}

以下是 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;
}

此函数创建包含工具按钮和标签的 QWidget。使用此函数创建的小部件用于工具箱中的背景选项卡小部件。

这里是 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;
}

此函数返回一个包含一个具有 QToolButtonQWidget,该按钮展示了一个 DiagramItems 的图像,即流程图形状。图像是通过 DiagramItem 通过 image() 函数创建的。使用 QButtonGroup 类,我们可以给每个按钮附加一个 id(整数);我们存储图表的类型,即 DiagramItem::DiagramType 枚举。当我们为场景创建新的图表项时,使用存储的图表类型。此函数创建的控件用于工具箱。

以下是 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;
}

此函数创建一个颜色菜单,用作 colorToolBar 中工具按钮的下拉菜单。我们为菜单中添加的每种颜色创建一个动作;当设置项目、线条和文字的颜色时,我们获取动作数据。

以下是 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);
}

此函数用于创建 QIconfillColorToolButtonfontColorToolButtonlineColorToolButton。字符串 imageFile 是按钮使用的文本、填充或线条符号。在图像下方,我们使用 color 绘制一个填充矩形。

以下是 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);
}

此函数创建一个色块填充的图标,颜色为 color。它用于在 fillColorToolButtonfontColorToolButtonlineColorToolButton 中创建颜色菜单的图标。

以下是 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();
}

在此函数中,我们设置了用于绘制图表场景背景的 QBrush。背景可以是蓝色、灰色或白色方块网格,或者根本无网格。我们有从 png 文件中创建的瓷砖的 QPixmap,我们用这些瓷砖创建画笔。

当点击背景标签控件中的某个按钮时,我们更改画笔;通过检查其文本来确定是哪个按钮。

以下是 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);
    }
}

当检查 buttonGroup 中的按钮时调用此槽。当按钮被选中时,用户可以点击图形视图,并将选中的类型的 DiagramItem 插入到 DiagramScene 中。我们必须遍历组中的按钮以取消选中其他按钮,因为我们一次只允许选中一个按钮。

QButtonGroup 为每个按钮分配一个 ID。我们将每个按钮的 ID 设置为图表类型,即 DiagramItem::DiagramType,当点击时将插入场景。然后我们可以使用按钮 ID 通过 setItemType() 设置图表类型。对于文字,我们分配了一个不在 DiagramType 枚举中的值。

以下是 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;
     }
}

此槽从场景中删除选定的项(如果有)。它首先删除箭头,以避免将它们删除两次。如果待删除的项是 DiagramItem,我们还需要删除与之连接的箭头;我们不想在场景中有不连接两端项的箭头。

以下是 pointerGroupClicked() 的实现

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

pointerTypeGroup 决定了场景是在项移动或插入线条模式。这个按钮组是互斥的,即任何时候只选中一个按钮。与上面的 buttonGroup 一样,我们已经给按钮分配了一个与 DiagramScene::Mode 枚举中的值相匹配的 ID,因此我们可以使用 ID 设置正确的模式。

以下是 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);
}

场景中的几个项目可能会相互碰撞,即重叠。当用户请求将项目置于与之碰撞的项目的上方时,将调用此槽。 QGraphicsItems 有一个 z 值,用于决定项目在场景中的堆叠顺序;你可以将其视为 3D 坐标系中的 z 轴。当项目碰撞时,z 值较高的项目将绘在 z 值较低的项目之上。当我们把一个项目调整到最前面时,我们可以遍历它与哪些项目碰撞,并将 z 值设置为高于所有这些项目。

这里是 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);
}

此槽与上面描述的 bringToFront() 以相同的方式工作,但设置一个低于应发送到后面的项目的 z 值。

以下是 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);
}

此槽在 DiagramScene 中添加项目时被调用。我们将场景的模式恢复到项目插入之前的模式,即 ItemMove 或 InsertText,具体取决于 pointerTypeGroup 中哪个按钮被选中。我们还必须取消选中 buttonGroup 中的按钮。

以下是 textInserted() 的实现

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

我们只是将场景模式恢复到插入文本之前的状态。

以下是 currentFontChanged() 槽的实现

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

当用户请求改变字体,通过使用 fontToolBar 中的某个小部件时,我们创建一个新的 QFont 对象,并将其属性设置与小部件的状态相匹配。这是在 handleFontChange() 中完成的,所以我们只需调用这个槽。

以下是 fontSizeChanged() 槽的实现

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

当用户请求改变字体,通过使用 fontToolBar 中的某个小部件时,我们创建一个新的 QFont 对象,并将其属性设置与小部件的状态相匹配。这是在 handleFontChange() 中完成的,所以我们只需调用这个槽。

以下是 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);
}

用户可以使用 sceneScaleCombo 来增加或减少场景的缩放级别,这时场景会被绘制出来。场景本身并没有改变其缩放级别,只是视图。

以下是 textColorChanged() 槽的实现

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

当点击 fontColorToolButton 的下拉菜单中的项目时,将调用此槽。我们需要将按钮上的图标更改为所选 QAction 的颜色。我们在 textAction 中保持对所选动作的指针。在 textButtonTriggered() 中,我们改变文本颜色为 textAction 的颜色,所以我们调用这个槽。

以下是 itemColorChanged() 的实现

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

此槽处理请求更改 DiagramItems 的颜色,其方式与 textColorChanged() 针对文本项的方式相同。

以下是 lineColorChanged() 槽的实现

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

此槽以与 textColorChanged()DiagramTextItems 的方式相同的方式处理更改箭头颜色的请求。

以下是 textButtonTriggered() 槽的实现

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

textAction 指向 fontColorToolButton 颜色下拉菜单中当前选定菜单项的 QAction。我们已将动作的数据设置为动作表示的 QColor,因此我们可以在设置文本颜色时使用 setTextColor() 获取它。

以下是 fillButtonTriggered() 槽的实现

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

fillAction 指向 fillColorToolButton 下拉菜单中选定的菜单项。我们可以使用此动作的数据,当使用 setItemColor() 设置项目颜色时。

以下是 lineButtonTriggered() 槽的实现

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

lineAction指向lineColorToolButton下拉菜单中选中的项。我们在使用setLineColor()设置箭头颜色时使用其数据。

以下是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()在显示字体属性的任何小部件发生变化时被调用。我们创建一个新的QFont对象,并根据小部件设置其属性。然后我们调用DiagramScenesetFont()函数;这是设置其管理的DiagramTextItems字体的场景。

以下是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());
}

此槽在DiagramScene中的任何项被选择时调用。在本例中,只有文本项在被选择时发出信号,因此我们不需要检查是什么类型的图形

我们设置小部件的状态以匹配所选文本项的字体属性。

这是about()槽函数

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

当用户从帮助菜单中选择关于菜单项时,此槽显示示例的关于框。

DiagramScene类定义

DiagramScene类继承自QGraphicsScene并添加了处理DiagramItems、ArrowsDiagramTextItems的功能,除了其超类处理的项目外。

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

DiagramScene中,鼠标点击可以执行三种不同的操作:鼠标下的项可以移动,可以插入项,或者可以在两个图项之间连接箭头。鼠标点击执行的操作取决于场景所在的模式,即Mode枚举中给出的模式。模式通过setMode()函数设置。

场景还设置其项的颜色和文本项的字体。场景使用的颜色和字体可以通过setLineColor()setTextColor()setItemColor()setFont()函数设置。在插入项时创建的、由DiagramItem::DiagramType函数给出的DiagramItem类型,通过setItemType()槽设置。

MainWindowDiagramScene分担示例功能的责任。MainWindow处理以下任务:删除项、文本和箭头;将图项移动到前面和后面;以及设置场景的比例。

DiagramScene类实现

我们从构造函数开始

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

场景使用myItemMenu在创建DiagramItems时设置上下文菜单。我们将默认模式设置为DiagramScene::MoveItem,因为这给出了QGraphicsScene的默认行为。

以下是setLineColor()函数

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

isItemChange函数返回true,如果在场景中选定了Arrow项,此时我们想要更改其颜色。当DiagramScene创建并将新箭头添加到场景时,它也将使用新的颜色

以下是setTextColor()函数

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

此函数将DiagramTextItems的颜色设置得与setLineColor()设置Arrows的颜色相同。

以下是setItemColor()函数

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

此功能 设置场景在创建 DiagramItems 时使用的颜色。它还会更改所选 DiagramItem 的颜色。

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

设置用于新和所选的字体,如果选中了一个文本项,则为 DiagramTextItems

这是 editorLostFocus() 插槽的实现

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

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

DiagramTextItems 在失去焦点时发出信号,该信号连接到此插槽。如果没有文本,我们会删除该项。如果没有,我们会泄露内存并使用户感到困惑,因为这些项将在被鼠标按下时编辑。

mousePressEvent() 函数根据 DiagramScene 所处的模式处理不同模式的鼠标按下事件。我们分别检查每种模式的实现。

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;

我们只是在鼠标按下位置创建一个新的 DiagramItem 并将其添加到场景中。请注意,其局部坐标系的原点将在鼠标指针位置下方。

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

用户通过在应连接的项之间拉伸线条将 Arrows 添加到场景中。线条的起始点固定在用户点击鼠标的位置,并且只要按钮按住,结束点就会跟随鼠标指针。当用户释放鼠标按钮时,如果线条的起始和结束位置下有 DiagramItem,则会在场景中添加一个 Arrow。我们将在稍后看到这是如何实现的;这里我们简单地添加线条。

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

当设置 Qt::TextEditorInteraction 标志时,DiagramTextItem 是可编辑的,否则它可以通过鼠标移动。我们总是希望文本被绘制在其他场景项之上,所以我们将其值设置为比场景中其他项更高的数字。

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

如果进入默认开关,表明我们处于 MoveItem 模式;然后我们可以调用 QGraphicsScene 实现,该实现处理项的鼠标拖动。即使我们处于其他模式,我们也会进行此调用,这使得在添加项后按住鼠标按钮并开始移动该项成为可能。对于文本项,这是不可能的,因为它们在可编辑时不传递鼠标事件。

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

如果我们处于插入模式且鼠标按钮按下(线条不是 0),则必须绘制线条。在 mousePressEvent() 中已讨论过,线条是从鼠标点击位置绘制到当前鼠标位置的。

如果我们处于移动项模式,则调用处理项移动的 QGraphicsScene 实现。

mouseReleaseEvent() 函数中,我们需要检查是否应向场景添加箭头

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;

首先,我们需要获取线条起始点和结束点下的项(如果有)。线条自身是这些点上的第一个项,所以我们要从列表中删除它。作为一种预防措施,我们检查列表是否为空,但这种事情不应该发生。

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

现在,我们检查线条起始点和结束点下是否有两个不同的 DiagramItems。如果有,我们可以使用两个项创建一个 Arrow。然后箭头分别添加到每个项和场景中。箭头必须更新以调整其起始点和结束点。我们将箭头的 z 值设置为 -1000.0,因为我们总是想让它比项更早被绘制。

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

以下是 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();
}

场景具有单选功能,即任何给定时间只能选择一个项。for循环随后将使用一个选中项进行一次迭代或在一个项未选中时进行零次迭代。isItemChange()用来检查是否存在已选择项,并且该项是否为指定的类型

绘图项类定义

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代表在DiagramScene中的流程图形状。它继承自QGraphicsPolygonItem,并为每个形状拥有一个多边形。枚举类型DiagramType为每个流程图形状提供了一个值。

该类包含一个与它相连的箭头列表。这是因为只有项知道何时移动(通过itemChanged()函数),此时箭头必须更新。项还可以使用image()函数将自己绘制到QPixmap上。这通常用于MainWindow的工具按钮,请参阅MainWindow中的createColorToolButtonIcon()

Type枚举是该类的唯一标识符。它被qgraphicsitem_cast()使用,该函数对图形项进行动态类型转换。用户类型常量是自定义图形项类型可以的最小值。

绘图项类实现

我们先看看构造函数

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

在构造函数中,我们根据diagramType创建项的多边形。QGraphicsItem默认不可移动或选择,因此我们必须设置这些属性。

以下是根据removeArrow()函数

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

removeArrow()在从场景中移除箭头项或与之相连的项时,用于移除Arrow项。

以下是根据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;
    }
}

在从场景移除项时调用此函数,移除与此项相连的所有箭头。必须从其开始项和结束项的arrows列表中删除箭头。由于开始项或结束项是该函数当前调用的对象,我们必须确保在移除箭头的同时修改此容器。

以下是根据addArrow()函数

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

此函数简单地将箭头添加到项的arrows列表中。

以下是根据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;
}

此函数将项的多边形绘制到QPixmap上。在此示例中,我们使用此函数来为工具箱中的工具按钮创建图标。

以下是根据contextMenuEvent()函数

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

显示上下文菜单。由于默认情况下鼠标右键点击显示菜单而不选择项,我们通过setSelected设置为选中项。这是必要的,因为必须选择项才能使用bringToFrontsendToBack动作更改其高度。

下面是itemChange()的实现

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

    return value;
}

如果项已移动,我们需要更新与该项相连的箭头的位置。由于QGraphicsItem的实现不执行任何操作,所以我们只是返回value

绘图文本项类定义

TextDiagramItem类继承自QGraphicsTextItem,并添加了移动可编辑文本项的功能。可编辑的QGraphicsTextItems旨在固定在原地,并且当用户单独点击项时开始编辑。使用DiagramTextItem,编辑在双击时开始,为单击留出空间以与项交互和移动。

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

我们使用 itemChange()focusOutEvent() 来通知 DiagramScene 当文本项失去焦点并被选中时。

我们重新实现了处理鼠标事件的函数,以便更改 QGraphicsTextItem 的鼠标行为。

DiagramTextItem 实现

我们从构造函数开始

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

我们简单地设置了项的可移动性和可选中性,因为这些标记默认是关闭的。

以下是 itemChange() 函数

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

当项被选中时,我们发出 selectedChanged 信号。 MainWindow 使用此信号来更新显示字体属性的控件到所选文本项的字体。

以下是 focusOutEvent() 函数

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

DiagramScene 使用文本项失去焦点时发出的信号,如果项为空(即不包含文本),则删除项。

以下是 mouseDoubleClickEvent() 的实现

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

当我们收到双击事件时,通过调用 QGraphicsTextItem::setTextInteractionFlags() 使项可编辑。然后,我们将双击事件转发给该项本身。

箭头类定义

Arrow 类是一个连接两个 DiagramItems 的图形项。它在一项中绘制一个箭头。为了实现这一点,该项需要自行绘制,并重新实现图形场景使用的用于检测碰撞和选择的代码。该类从 QGraphicsLineItem 继承,并绘制箭头头并与其连接的项一起移动。

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

可以使用 setColor() 设置项的颜色。

boundingRect()shape() 是从 QGraphicsLineItem 中重新实现,并由场景用于检测碰撞和选择。

调用 updatePosition() 会使箭头重新计算其位置和箭头头角度。重新实现了 paint(),使我们能够绘制箭头而不是在项之间的线条。

myStartItemmyEndItem 是箭头连接的图形项。箭头以尾项为头的方向绘制。 arrowHead 是一个有三个顶点的多边形,我们用它来绘制箭头头。

箭头类实现

Arrow 类的构造函数如下所示

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

我们设置箭头的起始和结束图形项。箭头头将在线与结束项相交处绘制。

以下是 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);
}

我们需要重写此函数,因为箭头的尺寸大于 QGraphicsLineItem 的边界矩形。图形场景使用边界矩形来知道需要更新场景的哪些区域。

以下是 shape() 函数

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

shape 函数返回一个 QPainterPath,这是项的确切形状。 QGraphicsLineItem::shape() 返回一个用当前画笔绘制的路径,所以我们只需添加箭头头。此函数用于与鼠标碰撞和选择。

以下是 updatePosition() 插槽

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

此插槽通过将线的起始和结束点设置为连接项的中心来更新箭头。

以下是 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);

如果起始和结束项发生碰撞,我们不绘制箭头;我们用来找到箭头绘制点的算法可能会因为项碰撞而失败。

首先,我们设置用于画箭头的笔和刷。

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

接下来,需要找到绘制箭头尖的位置。尖头应该画在直线和末项相交的地方。这是通过取多边形中每一点的线并检查它是否与箭头线相交来完成的。由于线的起点和终点被设置为项的中心,箭头线应仅与多边形中的某一条线相交。请注意,多边形的点与项的局部坐标系相关。因此,我们必须将末项的位置添加上去以使坐标相对于场景。

    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;

我们计算x轴和箭头线之间的角度。我们需要将箭头头指向这个角度,以便它跟随箭头的方向。如果角度是负的,我们必须改变箭头的方向。

然后我们可以计算箭头多边形的三个点。其中一个点是线的末端,现在它是箭头线和末端多边形的交点。然后我们清除之前计算出的箭头多边形,并设置这些新点。

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

如果选择了线,我们绘制两条与箭头线平行的虚线。我们不使用默认的实现,该实现使用boundingRect(),因为QRect边界矩形比线大得多。

示例项目 @ code.qt.io

© 2024 The Qt Company Ltd。本文件包含的文档贡献为各自所有者的版权。这里提供的文档是根据自由软件基金会发布的GNU自由文档许可协议版本1.3的条款许可的。Qt及其标志为芬兰和/或世界其他国家的The Qt Company Ltd的商标。所有其他商标均为各自所有者的财产。