QT 中 Graphics View 程序例子-Diagram Scene Example

news2024/11/17 1:58:26

一、 概况

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

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

        Qt图形视图框架设计用于管理和显示自定义2D图形项目。该框架的主要类是QGraphicsItem、QGraphicsScene和QGraphicsView。图形场景管理项目并为它们提供面。QGraphicsView是一个小部件,用于在屏幕上渲染场景。有关框架的更详细描述,请参阅图形视图框架。

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

        我们展示重要部分如何实现:

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

        该示例由以下类组成:

  •         章节二:MainWindow创建小部件并将其显示在QMainWindow中。它还管理小部件与图形场景、视图和项目之间的交互。
  •         章节三:DiagramScene继承了QGraphicsDiagramScene,并提供对DiagramItem、Arrow和DiagramTextItem的支持。
  •         章节四:DiagramItem继承QGraphicsPolygonItem并表示流程图形状。
  •         章节五:TextDiagramItem继承了QGraphicsTextItem并表示关系图中的文本项。该类添加了对使用鼠标移动项目的支持,而QGraphicsTextItem不支持这种支持。
  •         章节六:Arrow继承了QGraphicsLineItem,是连接两个DiagramItem的箭头。    

二 、 MainWindow类

        MainWindow和DiagramScene共同负责示例的功能。主窗口处理以下任务:删除项目、文本和箭头;移动图表项目到后面和前面;设定场景的规模。

2.1 MainWindos类定义

 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);
     QMenu *createColorMenu(const char *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;
     QAction *boldAction;
     QAction *underlineAction;
     QAction *italicAction;
     QAction *textAction;
     QAction *fillAction;
     QAction *lineAction;

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

        MainWindow类在QMainWindow中创建并布置小部件。该类将小部件的输入转发到DiagramScene。当或者将图表项或图表文本项插入到场景中时,它还会更新其小部件。

        该类还从场景中删除项目并处理z顺序,z顺序决定项目相互重叠时的绘制顺序。

  2.2 构造函数

   我们从构造函数开始:

 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,我们将使用它来更新显示字体属性的小部件。

2.3 工具箱

        createToolBox()函数创建并布置工具箱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"));
 }

        我们将工具箱的首选大小设置为最大值。这样,就为图形视图提供了更多的空间。

   2.4 动作    

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

        我们展示了一个创建动作的例子。操作触发的功能在我们连接操作的插槽中进行了讨论。如果您需要操作的高级介绍,可以查看应用程序示例。

  2.5 菜单

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

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

2.6 工具栏

        createToolbar()函数用于设置示例工具栏。colorToolBar中的三个QToolButton,即fontColorToolButton、fillColorToolButton和lineColorToolButton,非常有趣,因为我们通过使用QPainter在QPixmap上绘制来为它们创建图标。我们展示了fillColorToolButton是如何创建的。此按钮允许用户为图表项目选择颜色。

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

        我们用setMenu()设置工具按钮的菜单。我们需要fillActionQAction对象始终指向菜单的选定操作。菜单是用createColorMenu()函数创建的,正如我们稍后将看到的,它为项目可以具有的每种颜色包含一个菜单项。当用户按下触发clicked()信号的按钮时,我们可以将所选项目的颜色设置为fillAction的颜色。通过createColorToolButtonCon(),我们为按钮创建了图标

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

三、场景

3.1 头文件定义

        DiagramScene 类继承了 QGraphicsScene,并添加了处理 DiagramItems、Arrows 和 DiagramTextItems 的功能,以及其超类处理的项。

 class DiagramScene : public QGraphicsScene
 {
     Q_OBJECT

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

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

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

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

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

 private:
     bool isItemChange(int type) const;

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

        在DiagramScene中,鼠标单击可以给出三种不同的操作:可以移动鼠标下的项,可以插入项,或者可以在图项之间连接箭头。鼠标点击的动作取决于场景所处的模式(由Mode enum给出)。使用setMode()函数设置模式。
        场景还设置其项目的颜色和文本项目的字体。场景使用的颜色和字体可以通过setLineColor(), setTextColor(), settitemcolor()和setFont()函数来设置。DiagramItem的类型,由DiagramItem::DiagramType函数给出,当一个条目被插入时将被创建,并使用setItemType()槽设置。
        

3.2 构造函数

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

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

3.3 设置箭头图像线的颜色

 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,在这种情况下,我们希望更改其颜色。当 DiagramScene 创建并向场景添加新箭头时,它也将使用新颜色。

3.4 设置文本字的颜色

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

        此函数设置 DiagramTextItems 的颜色和setLineColor() 设置箭头颜色的方式类似。 

3.4 设置字体

 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和选定字体(如果选择了文本项)的字体。

3.5 失去焦点 

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

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

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

3.6 鼠标按下事件

        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;

        用户通过在箭头应连接的项之间拉伸一条线来将箭头添加到场景中。只要按住按钮,行的起点固定在用户单击鼠标的位置,终点就会跟随鼠标指针。当用户释放鼠标按钮时,如果行的开头和结尾下方有一个 DiagramItem,则会向场景中添加一个箭头。稍后我们将看到如何实现;在这里,我们只需添加该行。

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

        如果我们进入默认开关,我们处于移动项目模式;然后我们可以调用 QGraphicsScene 实现,它用鼠标处理项目的移动。即使我们处于另一种模式,我们也会进行此调用,从而可以添加一个项目,然后按住鼠标按钮并开始移动该项目。对于文本项,这是不可能的,因为它们在可编辑时不会传播鼠标事件。

3.7 鼠标移动事件

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()中所述,这条线是从鼠标被按下的位置绘制到鼠标的当前位置。

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

3.8 鼠标释放事件

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();
        }
    }
    line = nullptr;
    QGraphicsScene::mouseReleaseEvent(mouseEvent);
}

        现在我们检查行的起点和终点下是否有两个不同的图表项。如果有,我们可以创建一个包含这两个项目的箭头。然后将箭头添加到每个项目,最后添加到场景中。必须更新箭头以调整其项目的起点和终点。我们将箭头的 z 值设置为 -1000.0,因为我们始终希望将其绘制在项目下。

3.9 图像项目选择

 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() 用于检查所选项目是否存在,并且是否属于指定的图表类型。

四、图形项目

4.1 头文件定义

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;
    QVector<Arrow *> arrows;
};

        DiagramItem 表示 DiagramScene 中的流程图形状。它继承了QGraphicsPolygonItem,并且每个形状都有一个多边形。枚举 DiagramType 具有每个流程图形状的值。

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

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

4.2 构造函数

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 创建项多边形。默认情况下,QGraphicsItems 不可移动或选择,因此我们必须设置这些属性。

4.3 箭头列表操作

        增加箭头项目:

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

        removeArrow() 用于在从场景中移除箭头项或它们所连接的图项时删除箭头项。

void DiagramItem::removeArrow(Arrow *arrow)
{
    arrows.removeAll(arrow);
}
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;
    }
}

        当从场景中移除该项并移除连接到该项的所有箭头时,将调用此函数。必须从其开始项和结束项的箭头列表中删除箭头。由于开始项或结束项是当前调用此函数的对象,因此我们必须确保处理箭头的副本,因为 removeArrow() 正在修改此容器。 

4.4 图形的图标

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 上。在此示例中,我们使用它为工具箱中的工具按钮创建图标。 

4.5  图形右击菜单

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

        我们显示上下文菜单。由于鼠标右键单击显示菜单,默认情况下不要选择项目,我们使用 setSelected() 设置选择的项目。这是必需的,因为必须选择一个项目才能使用操作。

4.6  图形项目改变

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

    return value;
}

        如果项目已移动,我们需要更新与其连接的箭头的位置。QGraphicsItem 的实现什么都不做,所以我们只返回值。

五、文本图形

5.1 头文件定义

        TextDiagramItem 类继承于 QGraphicsTextItem,并增加了移动可编辑文本项的可能性。可编辑的 QGraphicsTextItem 被设计为固定位置,当用户单击该项目时即开始编辑。而使用 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 的鼠标行为。

5.2 构造函数

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

        我们只需将项目设置为可移动和可选择,因为这些标志默认是关闭的。

5.3 文本选择

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

        当项目被选中时,我们会发出 selectedChanged 信号。主窗口会使用该信号将显示字体属性的 widget 更新为所选文本项的字体。

5.4 失去焦点 

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

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

5.5 鼠标双击

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

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

六、箭头图形

6.1 头文件定义

        箭头类是连接两个 DiagramItems 的图形项。它为其中一个项目绘制一个箭头。为此,该项目需要绘制自身,并重新实现图形场景用于检查碰撞和选择的方法。该类继承于 QGraphicsLine item,绘制箭头并与所连接的项一起移动。

 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(),因此我们可以在项目之间绘制箭头而不仅仅是一条线。
        myStartItem 和 myEndItem 是箭头连接的图项。箭头是一个有三个顶点的多边形,我们用它来绘制箭头头。


6.2 构造函数

        箭头类的构造函数如下:

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

        我们设置箭头的起点和终点图项。箭头的头部将绘制在直线与结束项相交的地方。

6.3 边界矩形函数

         以下是 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 的边界矩形,因此我们需要重新实现该函数。图形场景使用边界矩形来确定要更新的场景区域。

6.4 碰撞和选择区域 

        下面是shape()函数

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

        shape 函数会返回一个与项目形状完全相同的 QPainterPath。QGraphicsLineItem::shape()返回的路径是用当前画笔绘制的直线,因此我们只需添加箭头的头部。该函数用于检查碰撞和鼠标选择。

6.5 位置更新 

        下面是updatePosition() 函数的槽 slot:

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

        此槽通过将箭头线的起点和终点设置为所连接项目的中心点来更新箭头。 

6.6 绘制函数

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

        如果选择了直线,我们就会绘制两条与箭头直线平行的虚线。由于 QRect 边界矩形比直线大得多,因此我们不使用默认的实现方式,即使用 boundingRect() 。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1139201.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

HackTheBox-Starting Point--Tier 1---Crocodile

文章目录 一 题目二 实验过程 一 题目 Tags Web、Network、Custom Applications、Protocols、Apache、FTP、Reconnaissance、Web Site Structure Discovery、Clear Text Credentials、Anonymous/Guest Access译文&#xff1a;Web、网络、定制应用程序、协议、Apache、FTP、侦…

C++项目——云备份-③-实用工具类设计与实现

文章目录 专栏导读1.文件实用工具类的设计2.文件实用工具类的实现2.1前置知识补充2.1.1struct stat 与 stat介绍2.1.2std::experimental::filesystem认识 2.2FileUtil实现 3.JSON实用工具类的设计4.JSON实用工具类的实现5.实用工具类整理 专栏导读 &#x1f338;作者简介&#…

ESP32智能小车+PS2无线遥控器+麦克纳姆轮+microPython

from machine import Pin,PWM from ps2 import PS2Controller import time import os# ############################################# # PS2 遥控器 # ############################################# ps2ctl PS2Controller(di_pin_no26, do_pin_no27, cs_pin_no14, clk_pin…

Unity中Shader的模型网格阴影

文章目录 前言一、网格阴影原理1、在世界空间下&#xff0c;把角色模型在Y轴上压缩成一个面片&#xff0c;把颜色修改成像影子的颜色2、把压缩后的面片&#xff0c;移动到合适的位置&#xff0c;把模型和阴影面片错开3、实现距离脚近的阴影偏移少&#xff0c;距离脚远的阴影偏移…

【已解决】AttributeError: module ‘cv2‘ has no attribute ‘bgsegm‘

问题 使用cv2.bgsegm.createBackgroundSubtractorMOG()去除背景的时候&#xff0c;遇到如下问题&#xff1a; AttributeError: module cv2 has no attribute bgsegm原因 报错原因&#xff1a;使用的python环境中没有安装扩展包contrib 解决方法 可以通过pip或者conda安装 …

QT中文乱码解决方案与乱码的原因

相信大家应该都遇到过中文乱码的问题&#xff0c;有时候改一改中文就不乱码了&#xff0c;但是有时候用同样的方式还是乱码&#xff0c;那么这个乱码到底是什么原因&#xff0c;又该如何彻底解决呢&#xff1f; 总结 先总结一下&#xff1a; Qt5中&#xff0c;将QString()的构…

Java实现Csv文件导入导出

Java实现Csv文件导入导出 什么是.csv文件&#xff1f; CSV&#xff08;Comma-Separated Values&#xff0c;逗号分隔的值&#xff09;是一种简单、实用的文件格式&#xff0c;用于存储和表示包括文本、数值等各种类型的数据。CSV 文件通常以 .csv 作为文件扩展名。这种文件格…

基于蜣螂优化算法DBO优化的VMD-KELM光伏发电短期功率预测MATLAB代码

微❤关注“电气仔推送”获得资料&#xff08;专享优惠&#xff09; VMD适用于处理非线性和非平稳信号&#xff0c;例如振动信号、生物信号、地震信号、图像信号等。它在信号处理、振动分析、图像处理等领域有广泛的应用&#xff0c;特别是在提取信号中的隐含信息和去除噪声方面…

字符串中的strcpy和strncpy区别

strcpy:函数原型是char *strcpy(char* dest, const char *src)&#xff0c;含义是将src中的字符串复制到dest中。 strncpy:函数原型是char *strncpy(char *dest const char *src,int n&#xff09;&#xff0c;表示把src所指向的字符串中以src地址开始的前n个字节复制到dest所…

香港施政报告人才引进政策2023全面解读,对优才计划申请是否有影响?

香港施政报告人才引进政策2023全面解读&#xff0c;对优才计划申请是否有影响&#xff1f; 香港第二份施政报告10月25日出来了&#xff01;这次真的是“走进民生”啊&#xff0c;什么路都帮你想好了&#xff01; 总结就是&#xff1a;继续抢人才、留人才&#xff01;在昨天的《…

103.linux5.15.198 编译 firefly-rk3399(2)

1. 平台&#xff1a; rk3399 firefly 2g16g 2. 内核&#xff1a;linux5.15.136 &#xff08;从内核镜像网站下载&#xff09; 3. 交叉编译工具 gcc version 7.5.0 (Ubuntu/Linaro 7.5.0-3ubuntu1~18.04) 4. 宿主机&#xff1a;ubuntu18.04 5. 需要的素材和资料&#xff…

MySQL 单表查询 多表设计

目录 数据库操作-DQL(单表查询)语法基本查询&#xff08;不带任何条件&#xff09;条件查询&#xff08;where&#xff09;聚合函数分组查询&#xff08;group by [having]&#xff09;&#xff08;重点&#xff09;排序查询&#xff08;order by&#xff09;&#xff08;重点&…

MySQL数据库基本操作2

文章目录 主要内容一.DQL1.语法格式代码如下&#xff08;示例&#xff09;: 2.数据准备代码如下&#xff08;示例&#xff09;: 3.简单查询代码如下&#xff08;示例&#xff09;: 4.运算符5.运算符操作-算术运算符代码如下&#xff08;示例&#xff09;: 6.运算符操作-条件查询…

Spring Cloud Sentinel整合Nacos实现配置持久化

sentinel配置相关配置后无法持久化&#xff0c;服务重启之后就没了&#xff0c;所以整合nacos&#xff0c;在nacos服务持久化&#xff0c;sentinel实时与nacos通信获取相关配置。 使用上一章节Feign消费者服务实现整合。 版本信息&#xff1a; nacos:1.4.1 Sentinel 控制台 …

【DRAM存储器十七】DDR2介绍-DDR2的新增技术-Post CAS、ODT、RDQS、OCD

&#x1f449;个人主页&#xff1a;highman110 &#x1f449;作者简介&#xff1a;一名硬件工程师&#xff0c;持续学习&#xff0c;不断记录&#xff0c;保持思考&#xff0c;输出干货内容 参考资料&#xff1a;《镁光DDR数据手册》 目录 Post CAS ODT RDQS OCD Post CA…

方舟生存进化ARK个人服务器搭建教程保姆级

方舟生存进化ARK个人服务器搭建教程保姆级 大家好我是艾西&#xff0c;在很久之前我有给大家分享过方舟生存进化的搭建架设教程&#xff0c;但时间久远且以前的教程我现在回头看去在某些地方说的并不是那么清楚。最近也是闲暇无事打算重新巩固下方舟生存进化的搭建架设教程&…

[计算机提升] Windows文件系统类型介绍

1.13 文件系统 在Windows系统中&#xff0c;文件系统是一种用于组织和管理计算机上存储的文件和目录的方法。它提供了一种结构化的方式来访问、存储和检索数据。 以下是Windows系统中常见的文件系统&#xff1a; FAT&#xff08;FAT16、FAT32&#xff09;&#xff1a;FAT&…

2023年中国研磨液需求量、市场规模及行业竞争格局分析[图]

研磨是半导体加工过程中的一项重要工艺&#xff0c;它主要是应用化学研磨液混配磨料的方式对半导体表面进行精密加工&#xff0c;研磨液是平坦化工艺中研磨材料和化学添加剂的混合物&#xff0c;研磨材料主要是石英、二氧化铝和氧化铈&#xff0c;研磨液是影响半导体表面质量的…

endnote设置

问题1&#xff1a;参考文献的tab太长 首先要在endnote里面这样设置&#xff0c;file->output->edit "XXX" 保存之后&#xff0c;在word更新目录。 在word里面设置悬挂缩进 结果&#xff1a; Endnote参考编号与参考文献距离太远怎么调整 endnote 文献对齐方式…

LibTorch实战一:MNIST的python代码

目录 一、下载MNIST-demo的cpp、python版本代码 二、五分钟读懂pytorch代码 三、下载MNIST数据集、训练模型 四、模型序列化、可视化分析 本文借用mnist这个相对简易深度学习任务来开始讲解libtorch如何部署模型。因此&#xff0c;这是一个如何编写libtorch代码的实战教程。…