场景示例图
演示如何使用 Graphics View 框架。

“图示场景”示例是一个可用于创建流程图的应用程序。您可以像上图所示那样添加流程图形状和文本,并用箭头连接这些形状。可以为形状、箭头和文本设置不同的颜色,还可以更改文本的字体、样式和下划线。
Qt 图形视图框架旨在管理和显示自定义的 2D 图形项。该框架的主要类包括QGraphicsItem 、QGraphicsScene 和QGraphicsView 。图形场景负责管理这些项,并为其提供显示区域。QGraphicsView 是一个用于将场景渲染到屏幕上的控件。有关该框架的更详细说明,请参阅《图形视图框架》。
在本示例中,我们将演示如何通过实现继承自 `QGraphicsScene ` 和 `QGraphicsItem` 的类来创建此类自定义图形场景和项目。
具体来说,我们将演示如何:
- 创建自定义图形项。
- 处理鼠标事件和对象的移动。
- 实现一个能够管理自定义项的图形场景。
- 对项目进行自定义绘制。
- 创建可移动且可编辑的文本控件。
该示例包含以下类:
MainWindow负责创建控件,并在QMainWindow 中显示它们。它还管理控件与图形场景、视图及项目之间的交互。DiagramItem继承自QGraphicsPolygonItem ,表示流程图图形。TextDiagramItem继承自QGraphicsTextItem ,表示图中的文本项。该类增加了使用鼠标移动项的功能,而QGraphicsTextItem 不支持此功能。Arrow继承自QGraphicsLineItem ,是一个连接两个DiagramItems的箭头。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() 函数用于设置示例中的工具栏。colorToolBar 中的三个QToolButton(fontColorToolButton 、fillColorToolButton 和lineColorToolButton )尤为值得关注,因为我们通过在QPixmap 上使用QPainter 绘制图标来为其创建图标。我们将演示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);我们使用setMenu()来设置该工具按钮的菜单。我们需要确保fillAction 对象(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;
}该函数返回一个QWidget ,其中包含一个QToolButton ,该 包含DiagramItems (即流程图图形)之一对应的图像。该图像由DiagramItem 通过image() 函数生成。QButtonGroup 类允许我们为每个按钮附加一个ID(int);我们存储图的类型,即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);
}该函数用于生成fillColorToolButton 、fontColorToolButton 和lineColorToolButton 的QIcon 。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 的矩形。它用于为fillColorToolButton 、fontColorToolButton 和lineColorToolButton 中的颜色菜单创建图标。
以下是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();
}在此函数中,我们设置用于绘制 diagramscene 背景的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 指定的图类型,该类型将在点击时被插入到场景中。 随后,我们在使用setItemType() 设置图表类型时,可以使用该按钮的ID。对于文本按钮,我们分配的ID值不在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 负责判断场景当前处于“ItemMove”模式还是“InsertLine”模式。该按钮组具有互斥性,即任何时候仅有一个按钮被选中。与上文的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);
}场景中可能存在多个项目相互碰撞(即重叠)。当用户要求将某个项目置于其碰撞对象之上时,该插槽会被调用。QGrapicsItems 具有一个 z 值,该值决定了项目在场景中的堆叠顺序; 你可以将其视为三维坐标系中的 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值都更低的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() 处理DiagramTextItems 请求的方式相同。
以下是lineColorChanged() 的实现:
void MainWindow::lineColorChanged()
{
lineAction = qobject_cast<QAction *>(sender());
lineColorToolButton->setIcon(createColorToolButtonIcon(
":/images/linecolor.png",
qvariant_cast<QColor>(lineAction->data())));
lineButtonTriggered();
}该插槽处理Arrows 颜色更改请求的方式,与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 ` 对象,并根据控件设置其属性。然后,我们调用 `DiagramScene` 的 `setFont() ` 函数;该场景负责设置其管理的 `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 中的某项被选中时,会调用此插槽。在本例中,只有文本项在被选中时会发出信号,因此我们无需检查item 属于何种图形类型。
我们将控件的状态设置为与所选文本项的字体属性相匹配。
这是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 、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枚举定义。模式通过setMode() 函数进行设置。
场景还会设置其元素的颜色以及文本元素的字体。场景使用的颜色和字体可通过setLineColor() 、setTextColor() 、setItemColor() 和setFont() 函数进行设置。当插入元素时要创建的元素类型(由DiagramItem 通过DiagramItem::DiagramType函数确定)可通过setItemType() 槽进行设置。
MainWindow 和DiagramScene 共同负责示例功能。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;
}场景在创建 `DiagramItems` 时,会使用 `myItemMenu ` 来设置上下文菜单。我们将默认模式设置为 `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 函数在场景中选中Arrow 项时返回true,此时我们需要更改其颜色。当DiagramScene 创建并向场景中添加新箭头时,它也会使用新的color 。
以下是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);
}
}如果当前处于 InsertMode 且鼠标按钮被按下(线段长度不为 0),则必须绘制该线段。如mousePressEvent() 中所述,该线段是从鼠标按下位置绘制到鼠标当前位置的。
如果处于 MoveItem 模式,则调用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() 函数用于检查是否存在选中的项目,并验证该项目是否属于指定的type 。
DiagramItem 类定义
class DiagramItem : public QGraphicsPolygonItem
{
public:
enum { Type = UserType + 15 };
enum DiagramType { Step, Conditional, StartEnd, Io };
DiagramItem(DiagramType diagramType, QMenu *contextMenu, QGraphicsItem *parent = nullptr);
void removeArrow(Arrow *arrow);
void removeArrows();
DiagramType diagramType() const { return myDiagramType; }
QPolygonF polygon() const { return myPolygon; }
void addArrow(Arrow *arrow);
QPixmap image() const;
int type() const override { return Type; }
protected:
void contextMenuEvent(QGraphicsSceneContextMenuEvent *event) override;
QVariant itemChange(GraphicsItemChange change, const QVariant &value) override;
private:
DiagramType myDiagramType;
QPolygonF myPolygon;
QMenu *myContextMenu;
QList<Arrow *> arrows;
};DiagramItem 表示DiagramScene 中的流程图形状。它继承自QGraphicsPolygonItem ,并为每个形状提供一个多边形。枚举 DiagramType 为每个流程图形状分配了一个值。
该类包含与其相连的箭头列表。这是必要的,因为只有该项自身知道何时被移动(通过itemChanged() 函数),此时必须更新箭头。该项还可以通过image() 函数将自身绘制到QPixmap 上。这用于MainWindow 中的工具按钮,请参阅MainWindow 中的createColorToolButtonIcon() 。
Type 枚举是该类的唯一标识符。它由qgraphicsitem_cast() 使用,该函数用于对图形项进行动态类型转换。UserType 常量是自定义图形项类型可以取的最小值。
DiagramItem 类的实现
我们先来看一下构造函数:
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 项或与其相连的DiagramItems 项从场景中移除时,将其从 中移除。
以下是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 列表中移除。由于调用此函数的对象可能是起点项也可能是终点项,因此我们必须确保对箭头的副本进行操作,因为removeArrow()会修改该容器。
以下是 `addArrow() ` 函数的实现:
void DiagramItem::addArrow(Arrow *arrow)
{
arrows.append(arrow);
}该函数仅将arrow 添加到items的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() 来选中该项目。这是必要的,因为必须先选中项目,才能通过bringToFront 和sendToBack 操作来更改其高度。
以下是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 即可。
DiagramTextItem 类定义
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 的鼠标行为。
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 。它会在其中一个项上绘制箭头。为此,该项需要自行绘制,并重写图形场景中用于检查碰撞和选择的方法。该类继承自QGraphicsLine项,会绘制箭头,并随其连接的项一同移动。
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 是箭头连接的图元。箭头绘制时,其箭头指向末端图元。arrowHead 是一个具有三个顶点的多边形,我们用它来绘制箭头。
Arrow 类的实现
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 轴与箭头线段之间的夹角。我们需要将箭头头旋转至该角度,使其与箭头方向一致。如果该角度为负值,则必须反转箭头方向。
随后,我们可以计算箭头多边形的三点。其中一点是线段的端点,即箭头线段与终点多边形的交点。然后,我们清除之前计算的箭头所用的arrowHead 多边形,并设置这些新点。
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 的边界矩形比该线段大得多。
© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.