图表场景示例
演示如何使用图形视图框架。
Diagram Scene 示例是一个可以创建流程图的应用程序。如上图所示,您可以添加流程图形状和文本,并用箭头将形状连接起来。图形、箭头和文本可以使用不同的颜色,还可以更改文本的字体、样式和下划线。
Qt 图形视图框架旨在管理和显示自定义的 2D 图形项目。该框架的主要类有QGraphicsItem 、QGraphicsScene 和QGraphicsView 。图形场景管理项目并为它们提供一个表面。QGraphicsView 是一个用于在屏幕上渲染场景的部件。有关该框架的更详细介绍,请参阅图形视图框架。
在本示例中,我们将展示如何通过实现继承QGraphicsScene 和QGraphicsItem 的类来创建自定义图形场景和项目。
我们将特别展示如何
- 创建自定义图形项目。
- 处理鼠标事件和项目移动。
- 实现可管理自定义项目的图形场景。
- 自定义绘制项目。
- 创建可移动和可编辑的文本项。
该示例由以下类组成:
MainWindow
QMainWindow它还管理部件与图形场景、视图和项目之间的交互。DiagramItem
继承于 并表示流程图形状。QGraphicsPolygonItemTextDiagramItem
继承于 ,表示图表中的文本项。该类增加了对使用鼠标移动项目的支持,而 不支持该功能。QGraphicsTextItem QGraphicsTextItemArrow
继承于 ,是连接两个 DiagramItems 的箭头。QGraphicsLineItemDiagramScene
继承于 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()
函数设置了标签式 widget 项中的按钮,稍后将对其进行检查。
背景选项卡 widget 项的按钮设置方法相同,因此我们跳过工具箱的创建:
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
、fontColorToolButton
、fillColorToolButton
和lineColorToolButton
中的三个QToolButton非常有趣,我们通过在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(); }
在此函数中,我们设置了用于绘制图表场景背景的QBrush 。背景可以是由蓝色、灰色或白色瓷砖组成的方格,也可以没有方格。我们从 png 文件中提取了QPixmap的方格,并使用这些方格创建画笔。
当点击背景选项卡 widget 项目中的某个按钮时,我们就会更换画笔;我们可以通过检查其文本来确定是哪个按钮。
下面是buttonGroupClicked()
的实现:
void MainWindow::buttonGroupClicked(QAbstractButton *button) { const QList<QAbstractButton *> buttons = buttonGroup->buttons(); for (QAbstractButton *myButton : buttons) { if (myButton != button) button->setChecked(false); } const int id = buttonGroup->id(button); if (id == InsertTextButton) { scene->setMode(DiagramScene::InsertText); } else { scene->setItemType(DiagramItem::DiagramType(id)); scene->setMode(DiagramScene::InsertItem); } }
当buttonGroup
中的按钮被选中时,该槽会被调用。当一个按钮被选中时,用户可以点击图形视图,一个所选类型的DiagramItem
将插入DiagramScene
。我们必须循环查看组中的按钮,以取消选中其他按钮,因为一次只允许选中一个按钮。
QButtonGroup
为每个按钮分配一个 id。我们将每个按钮的 id 设置为 DiagramItem::DiagramType 给出的图表类型,当点击该按钮时,它将被插入到场景中。然后,当我们使用 设置图表类型时,就可以使用按钮 id。在文本的情况下,我们分配的 id 值不在 DiagramType 枚举中。setItemType()
以下是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
中,鼠标点击可产生三种不同的操作:移动鼠标下的项目、插入项目或在图表项目间连接箭头。鼠标点击的动作取决于场景所处的模式(由模式枚举给出)。模式可通过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(); } }
如果在场景中选择了Arrow
项目,则isItemChange
函数返回 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()
slot 的实现:
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()
。
类型枚举是类的唯一标识符。它被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 添加到项目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 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
arrowHead
箭头类的实现
Arrow
类的构造函数如下所示:
Arrow::Arrow(DiagramItem *startItem, DiagramItem *endItem, QGraphicsItem *parent) : QGraphicsLineItem(parent), myStartItem(startItem), myEndItem(endItem) { setFlag(QGraphicsItem::ItemIsSelectable, true); setPen(QPen(myColor, 2, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin)); }
我们设置箭头的起点和终点图项。箭头的头部将绘制在直线与结束项相交的地方。
下面是boundingRect()
函数:
QRectF Arrow::boundingRect() const { qreal extra = (pen().width() + 20) / 2.0; return QRectF(line().p1(), QSizeF(line().p2().x() - line().p1().x(), line().p2().y() - line().p1().y())) .normalized() .adjusted(-extra, -extra, extra, extra); }
我们需要重新实现该函数,因为箭头比QGraphicsLineItem 的边界矩形大。图形场景使用边界矩形来确定要更新的场景区域。
下面是shape()
函数:
QPainterPath Arrow::shape() const { QPainterPath path = QGraphicsLineItem::shape(); path.addPolygon(arrowHead); return path; }
shape 函数返回一个与项目形状完全一致的QPainterPath 。QGraphicsLineItem::shape() 返回一个用当前笔画线的路径,因此我们只需添加箭头的头部。该函数用于检查碰撞和鼠标选择。
这里是updatePosition()
槽:
void Arrow::updatePosition() { QLineF line(mapFromItem(myStartItem, 0, 0), mapFromItem(myEndItem, 0, 0)); setLine(line); }
此槽通过将箭头线的起点和终点设置为所连接项目的中心点来更新箭头。
这里是paint()
函数:
void Arrow::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) { if (myStartItem->collidesWithItem(myEndItem)) return; QPen myPen = pen(); myPen.setColor(myColor); qreal arrowSize = 20; painter->setPen(myPen); painter->setBrush(myColor);
如果起点和终点项目发生碰撞,我们就不会绘制箭头;如果项目发生碰撞,我们用来查找箭头绘制点的算法可能会失败。
我们首先设置绘制箭头所使用的笔和画笔。
QLineF centerLine(myStartItem->pos(), myEndItem->pos()); QPolygonF endPolygon = myEndItem->polygon(); QPointF p1 = endPolygon.first() + myEndItem->pos(); QPointF intersectPoint; for (int i = 1; i < endPolygon.count(); ++i) { QPointF p2 = endPolygon.at(i) + myEndItem->pos(); QLineF polyLine = QLineF(p1, p2); QLineF::IntersectionType intersectionType = polyLine.intersects(centerLine, &intersectPoint); if (intersectionType == QLineF::BoundedIntersection) break; p1 = p2; } setLine(QLineF(intersectPoint, myStartItem->pos()));
然后,我们需要找到绘制箭头的位置。箭头应画在直线和末端项目相交的位置。具体做法是取多边形中每个点之间的直线,并检查它是否与箭头的直线相交。由于线条的起点和终点都设置为项的中心点,因此箭头线条应该只与多边形中的一条线相交。请注意,多边形中的点是相对于项目的本地坐标系而言的。因此,我们必须加上末端项目的位置,使坐标相对于场景。
double angle = std::atan2(-line().dy(), line().dx()); QPointF arrowP1 = line().p1() + QPointF(sin(angle + M_PI / 3) * arrowSize, cos(angle + M_PI / 3) * arrowSize); QPointF arrowP2 = line().p1() + QPointF(sin(angle + M_PI - M_PI / 3) * arrowSize, cos(angle + M_PI - M_PI / 3) * arrowSize); arrowHead.clear(); arrowHead << line().p1() << arrowP1 << arrowP2;
我们计算 x 轴和箭头线之间的角度。我们需要将箭头转向这个角度,使其与箭头的方向一致。如果角度为负数,我们就必须转动箭头的方向。
然后我们就可以计算出箭头多边形的三个点。其中一个点是线的末端,也就是箭线和多边形末端的交点。然后,我们清除之前计算出的箭 头多边形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 边界矩形比直线大得多。
© 2025 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.