一、 概况
本例演示如何使用图形视图框架。
“图表场景”示例是一个应用程序,您可以在其中创建流程图。可以添加流程图形状和文本,并通过箭头连接形状,如上图所示。形状、箭头和文本可以赋予不同的颜色,并且可以更改文本的字体、样式和下划线。
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() 。