弹性节点示例

演示如何与场景中的图形项目进行交互。

弹性节点示例展示了如何通过基本交互实现图形中节点之间的边缘。您可以点击拖动节点,并使用鼠标滚轮或键盘放大或缩小。点击空格键将随机化节点。该示例还与分辨率无关;当你放大时,图形依然清晰。

图形视图提供了QGraphicsScene 类,用于管理大量源自QGraphicsItem 类的自定义 2D 图形项目并与之交互,还提供了一个QGraphicsView 部件,用于可视化这些项目,并支持缩放和旋转。

本示例由Node 类、Edge 类、GraphWidget 测试和main 函数组成:Node 类表示网格中可拖动的黄色节点,Edge 类表示节点之间的线条,GraphWidget 类表示应用程序窗口,main() 函数创建和显示该窗口,并运行事件循环。

节点类定义

Node 类有三个作用:

  • 在两种状态下绘制黄色渐变 "球":凹陷和凸起。
  • 管理与其他节点的连接。
  • 计算网格中节点的拉力和推力。

让我们先看看Node 类的声明。

class Node : public QGraphicsItem
{
public:
    Node(GraphWidget *graphWidget);

    void addEdge(Edge *edge);
    QList<Edge *> edges() const;

    enum { Type = UserType + 1 };
    int type() const override { return Type; }

    void calculateForces();
    bool advancePosition();

    QRectF boundingRect() const override;
    QPainterPath shape() const override;
    void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;

protected:
    QVariant itemChange(GraphicsItemChange change, const QVariant &value) override;

    void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
    void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override;

private:
    QList<Edge *> edgeList;
    QPointF newPos;
    GraphWidget *graph;
};

Node 类继承于QGraphicsItem ,并重新实现了boundingRect() 和paint() 这两个强制函数,以提供视觉外观。它还重新实现了shape() 以确保其命中区域具有椭圆形(而不是默认的矩形边界)。

在边缘管理方面,节点提供了一个简单的应用程序接口,用于向节点添加边缘并列出所有连接的边缘。

每当场景状态前进一步,就会调用advance() 重新实现。calculateForces()函数被调用,以计算对该节点及其邻居的推力和拉力。

Node 类还对itemChange() 进行了重实现,以对状态变化(本例中为位置变化)做出反应,并对mousePressEvent() 和mouseReleaseEvent() 进行了重实现,以更新项目的视觉外观。

我们将从Node 的构造函数开始回顾其实现:

Node::Node(GraphWidget *graphWidget)
    : graph(graphWidget)
{
    setFlag(ItemIsMovable);
    setFlag(ItemSendsGeometryChanges);
    setCacheMode(DeviceCoordinateCache);
    setZValue(-1);
}

在构造函数中,我们设置了ItemIsMovable 标志以允许项目响应鼠标拖动而移动,并设置了ItemSendsGeometryChanges 以启用itemChange() 位置和变换变化通知。我们还启用了DeviceCoordinateCache ,以加快渲染性能。为了确保节点始终堆叠在边缘之上,我们最后将项目的 Z 值设置为-1。

Node我们的构造函数获取了一个GraphWidget 指针,并将其存储为一个成员变量。稍后我们将再次访问该指针。

void Node::addEdge(Edge *edge)
{
    edgeList << edge;
    edge->adjust();
}

QList<Edge *> Node::edges() const
{
    return edgeList;
}

addEdge() 函数将输入的边缘添加到附加边缘列表中。然后对该边进行调整,使其终点与源节点和目的节点的位置相匹配。

edges() 函数只是返回附加边的列表。

void Node::calculateForces()
{
    if (!scene() || scene()->mouseGrabberItem() == this) {
        newPos = pos();
        return;
    }

移动节点有两种方法。calculateForces() 函数实现了在网格中拉动和推动节点的弹性效果。此外,用户还可以直接用鼠标移动节点。由于我们不希望这两种方法同时作用于同一个节点,因此在开始calculateForces() 时,我们会检查该Node 是否是当前的鼠标抓取项(即QGraphicsScene::mouseGrabberItem() )。由于我们需要找到所有相邻(但不一定相连)的节点,因此我们首先要确保该项目是场景的一部分。

    // Sum up all forces pushing this item away
    qreal xvel = 0;
    qreal yvel = 0;
    const QList<QGraphicsItem *> items = scene()->items();
    for (QGraphicsItem *item : items) {
        Node *node = qgraphicsitem_cast<Node *>(item);
        if (!node)
            continue;

        QPointF vec = mapToItem(node, 0, 0);
        qreal dx = vec.x();
        qreal dy = vec.y();
        double l = 2.0 * (dx * dx + dy * dy);
        if (l > 0) {
            xvel += (dx * 150.0) / l;
            yvel += (dy * 150.0) / l;
        }
    }

弹性 "效果来自于一种应用推力和拉力的算法。这种效果令人印象深刻,而实现起来却出奇地简单。

算法分为两步:第一步是计算将节点推开的力,第二步是减去将节点拉拢的力。首先,我们需要找到图中的所有节点。我们调用QGraphicsScene::items() 查找场景中的所有项目,然后使用qgraphicsitem_cast() 查找Node 实例。

我们使用mapFromItem() 以本地坐标创建一个从该节点指向其他节点的临时向量。我们使用该矢量的分解分量来确定节点受力的方向和强度。每个节点的力都会累加,然后进行调整,使最近的节点获得最强的力,当距离增加时,力会迅速减弱。所有力的总和存储在xvel (X-速度)和yvel (Y-速度)中。

    // Now subtract all forces pulling items together
    double weight = (edgeList.size() + 1) * 10;
    for (const Edge *edge : std::as_const(edgeList)) {
        QPointF vec;
        if (edge->sourceNode() == this)
            vec = mapToItem(edge->destNode(), 0, 0);
        else
            vec = mapToItem(edge->sourceNode(), 0, 0);
        xvel -= vec.x() / weight;
        yvel -= vec.y() / weight;
    }

节点之间的边代表将节点拉到一起的力。通过访问与该节点相连的每一条边,我们可以使用与上述类似的方法找到所有拉力的方向和强度。从xvelyvel 中减去这些力。

    if (qAbs(xvel) < 0.1 && qAbs(yvel) < 0.1)
        xvel = yvel = 0;

理论上,推力和拉力的总和应精确稳定为 0。为了避免数值精确度上的误差,我们只需在力的总和小于 0.1 时将其强制为 0。

    QRectF sceneRect = scene()->sceneRect();
    newPos = pos() + QPointF(xvel, yvel);
    newPos.setX(qMin(qMax(newPos.x(), sceneRect.left() + 10), sceneRect.right() - 10));
    newPos.setY(qMin(qMax(newPos.y(), sceneRect.top() + 10), sceneRect.bottom() - 10));
}

calculateForces() 的最后一步是确定节点的新位置。我们将力加到节点的当前位置。我们还要确保新位置位于定义的边界内。在此函数中,我们并不实际移动项目;这是在advance() 中的另一个步骤中完成的。

bool Node::advancePosition()
{
    if (newPos == pos())
        return false;

    setPos(newPos);
    return true;
}

advance() 函数更新项目的当前位置。该函数从GraphWidget::timerEvent() 中调用。如果节点的位置发生变化,函数返回 true;否则返回 false。

QRectF Node::boundingRect() const
{
    qreal adjust = 2;
    return QRectF( -10 - adjust, -10 - adjust, 23 + adjust, 23 + adjust);
}

Node 的边界矩形是一个 20x20 大小的矩形,以其原点(0,0)为中心,在所有方向上调整 2 个单位,以补偿节点的轮廓描边,并向下和向右调整 3 个单位,为简单的阴影留出空间。

QPainterPath Node::shape() const
{
    QPainterPath path;
    path.addEllipse(-10, -10, 20, 20);
    return path;
}

形状是一个简单的椭圆。这确保了必须在节点的椭圆形内单击才能拖动它。您可以通过运行示例来测试这种效果,然后放大节点,使其变得非常大。如果不重新实现shape(),项目的命中区域将与其边界矩形(即矩形)完全相同。

void Node::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *)
{
    painter->setPen(Qt::NoPen);
    painter->setBrush(Qt::darkGray);
    painter->drawEllipse(-7, -7, 20, 20);

    QRadialGradient gradient(-3, -3, 10);
    if (option->state & QStyle::State_Sunken) {
        gradient.setCenter(3, 3);
        gradient.setFocalPoint(3, 3);
        gradient.setColorAt(1, QColor(Qt::yellow).lighter(120));
        gradient.setColorAt(0, QColor(Qt::darkYellow).lighter(120));
    } else {
        gradient.setColorAt(0, Qt::yellow);
        gradient.setColorAt(1, Qt::darkYellow);
    }
    painter->setBrush(gradient);

    painter->setPen(QPen(Qt::black, 0));
    painter->drawEllipse(-10, -10, 20, 20);
}

该函数实现了节点的绘制。首先,我们在(-7, -7)处绘制一个简单的深灰色椭圆阴影,即从椭圆左上角(-10, -10)向下和向右(3, 3)个单位。

然后,我们绘制一个径向渐变填充的椭圆。该填充在升高时为Qt::yellowQt::darkYellow ,在下沉时则相反。在凹陷状态下,我们还会将中心点和焦点移动 (3, 3),以强调有东西被压下的印象。

使用渐变绘制填充椭圆可能会比较慢,尤其是在使用复杂渐变(如QRadialGradient )时。因此,本示例使用DeviceCoordinateCache 这一简单而有效的方法来防止不必要的重绘。

QVariant Node::itemChange(GraphicsItemChange change, const QVariant &value)
{
    switch (change) {
    case ItemPositionHasChanged:
        for (Edge *edge : std::as_const(edgeList))
            edge->adjust();
        graph->itemMoved();
        break;
    default:
        break;
    };

    return QGraphicsItem::itemChange(change, value);
}

我们重新实现itemChange() 来调整所有相连边缘的位置,并通知场景某个项目已经移动(即 "发生了一些事情")。这将触发新的力计算。

这种通知是节点需要保留指向GraphWidget 的指针的唯一原因。另一种方法是使用信号提供这种通知;在这种情况下,Node 需要继承于QGraphicsObject

void Node::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
    update();
    QGraphicsItem::mousePressEvent(event);
}

void Node::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
{
    update();
    QGraphicsItem::mouseReleaseEvent(event);
}

因为我们已经设置了ItemIsMovable 标志,所以我们不需要实现根据鼠标输入移动节点的逻辑;这已经为我们提供了。不过,我们仍然需要重新实现鼠标按下和松开处理程序,以更新节点的视觉外观(即凹陷或凸起)。

边缘类定义

Edge 类表示本示例中节点之间的箭头线。该类非常简单:它维护一个源节点和目的节点指针,并提供一个adjust() 函数,确保线条从源节点的位置开始,在目的节点的位置结束。只有边会随着对节点的拉力和推力而不断变化。

让我们来看看类的声明:

class Edge : public QGraphicsItem
{
public:
    Edge(Node *sourceNode, Node *destNode);

    Node *sourceNode() const;
    Node *destNode() const;

    void adjust();

    enum { Type = UserType + 2 };
    int type() const override { return Type; }

protected:
    QRectF boundingRect() const override;
    void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;

private:
    Node *source, *dest;

    QPointF sourcePoint;
    QPointF destPoint;
    qreal arrowSize = 10;
};

Edge 继承于 ,因为它是一个简单的类,不使用信号、槽和属性(与 相比)。QGraphicsItem QGraphicsObject

构造函数将两个节点指针作为输入。在本例中,两个指针都是必须的。我们还为每个节点提供了 get 函数。

adjust() 函数重新定位了边,该项目还实现了boundingRect() 和paint() 。

现在我们来回顾一下它的实现。

Edge::Edge(Node *sourceNode, Node *destNode)
    : source(sourceNode), dest(destNode)
{
    setAcceptedMouseButtons(Qt::NoButton);
    source->addEdge(this);
    dest->addEdge(this);
    adjust();
}

Edge 构造函数将其arrowSize 数据成员初始化为 10 个单位;这决定了在paint() 中绘制的箭头的大小。

在构造函数主体中,我们调用setAcceptedMouseButtons(0)。这将确保边缘项完全不会被视为鼠标输入(即无法点击边缘)。然后,更新源指针和目标指针,在每个节点上注册这条边,并调用adjust() 更新这条边的开始和结束位置。

Node *Edge::sourceNode() const
{
    return source;
}

Node *Edge::destNode() const
{
    return dest;
}

源点和目的点获取函数只是返回各自的指针。

void Edge::adjust()
{
    if (!source || !dest)
        return;

    QLineF line(mapFromItem(source, 0, 0), mapFromItem(dest, 0, 0));
    qreal length = line.length();

    prepareGeometryChange();

    if (length > qreal(20.)) {
        QPointF edgeOffset((line.dx() * 10) / length, (line.dy() * 10) / length);
        sourcePoint = line.p1() + edgeOffset;
        destPoint = line.p2() - edgeOffset;
    } else {
        sourcePoint = destPoint = line.p1();
    }
}

adjust() 中,我们定义了两个点:sourcePointdestPoint ,分别指向源节点和目的节点的起点。每个点都使用本地坐标计算。

我们希望边缘箭头的尖端指向节点的精确轮廓,而不是节点的中心。要找到这一点,我们首先要将从源点中心指向目的节点中心的矢量分解为 X 和 Y,然后用矢量的长度除以分量,将其归一化。这样就得到了一个 X 和 Y 单位三角洲,再乘以节点半径(10),就得到了必须添加到边缘一点并从另一点减去的偏移量。

如果矢量的长度小于 20(即两个节点重叠),那么我们就将源节点和目标节点的指针固定在源节点的中心。实际上,这种情况很难手动重现,因为此时两个节点之间的力是最大的。

值得注意的是,我们在这个函数中调用了prepareGeometryChange() 。原因是在绘制时会直接使用sourcePointdestPoint 变量,它们是从boundingRect() 重新实现返回的。在改变boundingRect() 的返回值以及paint() 使用这些变量之前,我们必须始终调用prepareGeometryChange() 以保持 Graphics View 内部账簿的整洁。最安全的做法是在修改任何此类变量之前立即调用一次该函数。

QRectF Edge::boundingRect() const
{
    if (!source || !dest)
        return QRectF();

    qreal penWidth = 1;
    qreal extra = (penWidth + arrowSize) / 2.0;

    return QRectF(sourcePoint, QSizeF(destPoint.x() - sourcePoint.x(),
                                      destPoint.y() - sourcePoint.y()))
        .normalized()
        .adjusted(-extra, -extra, extra, extra);
}

边的边界矩形定义为包含边的起点和终点的最小矩形。由于我们要在每条边缘上绘制一个箭头,因此还需要在所有方向上以箭头大小的一半和钢笔宽度的一半进行补偿调整。钢笔用于绘制箭头的轮廓,我们可以假设一半的轮廓可以绘制在箭头区域之外,另一半将绘制在箭头区域之内。

void Edge::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *)
{
    if (!source || !dest)
        return;

    QLineF line(sourcePoint, destPoint);
    if (qFuzzyCompare(line.length(), qreal(0.)))
        return;

在重新实现paint() 时,我们首先要检查几个前提条件。首先,如果没有设置源节点或目标节点,我们将立即返回;因为没有任何东西可以绘制。

同时,我们会检查边的长度是否约为 0,如果约为 0,我们也会返回。

    // Draw the line itself
    painter->setPen(QPen(Qt::black, 1, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
    painter->drawLine(line);

我们使用具有圆形接合点和笔帽的笔来绘制线条。如果运行示例,放大并仔细研究边缘,就会发现没有尖锐或方形的边缘。

    // Draw the arrows
    double angle = std::atan2(-line.dy(), line.dx());

    QPointF sourceArrowP1 = sourcePoint + QPointF(sin(angle + M_PI / 3) * arrowSize,
                                                  cos(angle + M_PI / 3) * arrowSize);
    QPointF sourceArrowP2 = sourcePoint + QPointF(sin(angle + M_PI - M_PI / 3) * arrowSize,
                                                  cos(angle + M_PI - M_PI / 3) * arrowSize);
    QPointF destArrowP1 = destPoint + QPointF(sin(angle - M_PI / 3) * arrowSize,
                                              cos(angle - M_PI / 3) * arrowSize);
    QPointF destArrowP2 = destPoint + QPointF(sin(angle - M_PI + M_PI / 3) * arrowSize,
                                              cos(angle - M_PI + M_PI / 3) * arrowSize);

    painter->setBrush(Qt::black);
    painter->drawPolygon(QPolygonF() << line.p1() << sourceArrowP1 << sourceArrowP2);
    painter->drawPolygon(QPolygonF() << line.p2() << destArrowP1 << destArrowP2);
}

我们接着在边缘的两端各画一个箭头。每个箭头都绘制为黑色填充的多边形。箭头的坐标使用简单的三角法确定。

GraphWidget 类定义

GraphWidget 是 的子类,它为主窗口提供滚动条。QGraphicsView

class GraphWidget : public QGraphicsView
{
    Q_OBJECT

public:
    GraphWidget(QWidget *parent = nullptr);

    void itemMoved();

public slots:
    void shuffle();
    void zoomIn();
    void zoomOut();

protected:
    void keyPressEvent(QKeyEvent *event) override;
    void timerEvent(QTimerEvent *event) override;
#if QT_CONFIG(wheelevent)
    void wheelEvent(QWheelEvent *event) override;
#endif
    void drawBackground(QPainter *painter, const QRectF &rect) override;

    void scaleView(qreal scaleFactor);

private:
    QBasicTimer timer;
    Node *centerNode;
};

该类提供了一个用于初始化场景的基本构造函数、一个用于通知场景节点图变化的itemMoved() 函数、几个事件处理程序、对drawBackground() 的重新实现,以及一个用于使用鼠标滚轮或键盘缩放视图的辅助函数。

GraphWidget::GraphWidget(QWidget *parent)
    : QGraphicsView(parent)
{
    QGraphicsScene *scene = new QGraphicsScene(this);
    scene->setItemIndexMethod(QGraphicsScene::NoIndex);
    scene->setSceneRect(-200, -200, 400, 400);
    setScene(scene);
    setCacheMode(CacheBackground);
    setViewportUpdateMode(BoundingRectViewportUpdate);
    setRenderHint(QPainter::Antialiasing);
    setTransformationAnchor(AnchorUnderMouse);
    scale(qreal(0.8), qreal(0.8));
    setMinimumSize(400, 400);
    setWindowTitle(tr("Elastic Nodes"));

GraphicsWidget构建器创建了场景,由于大多数项目在大部分时间内都会移动,因此构建器设置了QGraphicsScene::NoIndex 。然后,场景会获得一个固定的scene rectangle ,并分配给GraphWidget 视图。

视图使QGraphicsView::CacheBackground 能够缓存其静态且有些复杂的背景渲染。由于图形渲染的都是移动的小项目,图形视图没有必要浪费时间寻找精确的更新区域,因此我们设置了QGraphicsView::BoundingRectViewportUpdate 视口更新模式。默认模式可以正常工作,但在本示例中,该模式明显更快。

为了提高渲染质量,我们设置了QPainter::Antialiasing

变换锚点决定了变换视图时视图的滚动方式,在我们的例子中,变换锚点决定了放大或缩小时视图的滚动方式。我们选择了QGraphicsView::AnchorUnderMouse ,它可以将视图集中在鼠标光标下的点上。这样,通过将鼠标移至场景中的某一点,然后滚动鼠标滚轮,就可以轻松放大该点。

最后,我们赋予窗口一个与场景默认大小相匹配的最小尺寸,并设置一个合适的窗口标题。

    Node *node1 = new Node(this);
    Node *node2 = new Node(this);
    Node *node3 = new Node(this);
    Node *node4 = new Node(this);
    centerNode = new Node(this);
    Node *node6 = new Node(this);
    Node *node7 = new Node(this);
    Node *node8 = new Node(this);
    Node *node9 = new Node(this);
    scene->addItem(node1);
    scene->addItem(node2);
    scene->addItem(node3);
    scene->addItem(node4);
    scene->addItem(centerNode);
    scene->addItem(node6);
    scene->addItem(node7);
    scene->addItem(node8);
    scene->addItem(node9);
    scene->addItem(new Edge(node1, node2));
    scene->addItem(new Edge(node2, node3));
    scene->addItem(new Edge(node2, centerNode));
    scene->addItem(new Edge(node3, node6));
    scene->addItem(new Edge(node4, node1));
    scene->addItem(new Edge(node4, centerNode));
    scene->addItem(new Edge(centerNode, node6));
    scene->addItem(new Edge(centerNode, node8));
    scene->addItem(new Edge(node6, node9));
    scene->addItem(new Edge(node7, node4));
    scene->addItem(new Edge(node8, node7));
    scene->addItem(new Edge(node9, node8));

    node1->setPos(-50, -50);
    node2->setPos(0, -50);
    node3->setPos(50, -50);
    node4->setPos(-50, 0);
    centerNode->setPos(0, 0);
    node6->setPos(50, 0);
    node7->setPos(-50, 50);
    node8->setPos(0, 50);
    node9->setPos(50, 50);
}

构造函数的最后一部分创建节点和边的网格,并给每个节点一个初始位置。

void GraphWidget::itemMoved()
{
    using namespace std::chrono_literals;

    if (!timer.isActive())
        timer.start(1000ms / 25, this);
}

GraphWidget 通过 函数,主程序会收到节点移动的通知。它的工作只是在主定时器尚未运行的情况下重新启动主定时器。定时器的设计是在图形稳定时停止,并在图形再次不稳定时启动。itemMoved()

void GraphWidget::keyPressEvent(QKeyEvent *event)
{
    switch (event->key()) {
    case Qt::Key_Up:
        centerNode->moveBy(0, -20);
        break;
    case Qt::Key_Down:
        centerNode->moveBy(0, 20);
        break;
    case Qt::Key_Left:
        centerNode->moveBy(-20, 0);
        break;
    case Qt::Key_Right:
        centerNode->moveBy(20, 0);
        break;
    case Qt::Key_Plus:
        zoomIn();
        break;
    case Qt::Key_Minus:
        zoomOut();
        break;
    case Qt::Key_Space:
    case Qt::Key_Enter:
        shuffle();
        break;
    default:
        QGraphicsView::keyPressEvent(event);
    }
}

这是GraphWidget 的按键事件处理程序。箭头键左右移动中心节点,"+"和"-"键通过调用scaleView() 来放大和缩小,回车键和空格键随机调整节点的位置。所有其他按键事件(如向上翻页和向下翻页)都由QGraphicsView 的默认实现来处理。

void GraphWidget::timerEvent(QTimerEvent *event)
{
    Q_UNUSED(event);

    QList<Node *> nodes;
    const QList<QGraphicsItem *> items = scene()->items();
    for (QGraphicsItem *item : items) {
        if (Node *node = qgraphicsitem_cast<Node *>(item))
            nodes << node;
    }

    for (Node *node : std::as_const(nodes))
        node->calculateForces();

    bool itemsMoved = false;
    for (Node *node : std::as_const(nodes)) {
        if (node->advancePosition())
            itemsMoved = true;
    }

    if (!itemsMoved)
        timer.stop();
}

定时器事件处理程序的任务是以流畅的动画形式运行整个力计算机制。每次触发定时器时,处理程序都会查找场景中的所有节点,并逐一调用Node::calculateForces() 。然后,在最后一步,它将调用Node::advance() 将所有节点移动到新的位置。通过检查advance() 的返回值,我们可以判断网格是否稳定(即没有节点移动)。如果是,我们就可以停止计时器。

void GraphWidget::wheelEvent(QWheelEvent *event)
{
    scaleView(pow(2., -event->angleDelta().y() / 240.0));
}

在滚轮事件处理程序中,我们将鼠标滚轮的三角洲值转换为比例因子,并将该因子传递给scaleView() 。这种方法考虑到了滚轮滚动的速度。滚动鼠标滚轮的速度越快,视图缩放的速度就越快。

void GraphWidget::drawBackground(QPainter *painter, const QRectF &rect)
{
    Q_UNUSED(rect);

    // Shadow
    QRectF sceneRect = this->sceneRect();
    QRectF rightShadow(sceneRect.right(), sceneRect.top() + 5, 5, sceneRect.height());
    QRectF bottomShadow(sceneRect.left() + 5, sceneRect.bottom(), sceneRect.width(), 5);
    if (rightShadow.intersects(rect) || rightShadow.contains(rect))
        painter->fillRect(rightShadow, Qt::darkGray);
    if (bottomShadow.intersects(rect) || bottomShadow.contains(rect))
        painter->fillRect(bottomShadow, Qt::darkGray);

    // Fill
    QLinearGradient gradient(sceneRect.topLeft(), sceneRect.bottomRight());
    gradient.setColorAt(0, Qt::white);
    gradient.setColorAt(1, Qt::lightGray);
    painter->fillRect(rect.intersected(sceneRect), gradient);
    painter->setBrush(Qt::NoBrush);
    painter->drawRect(sceneRect);

    // Text
    QRectF textRect(sceneRect.left() + 4, sceneRect.top() + 4,
                    sceneRect.width() - 4, sceneRect.height() - 4);
    QString message(tr("Click and drag the nodes around, and zoom with the mouse "
                       "wheel or the '+' and '-' keys"));

    QFont font = painter->font();
    font.setBold(true);
    font.setPointSize(14);
    painter->setFont(font);
    painter->setPen(Qt::lightGray);
    painter->drawText(textRect.translated(2, 2), message);
    painter->setPen(Qt::black);
    painter->drawText(textRect, message);
}

视图的背景是通过重新实现QGraphicsView::drawBackground() 渲染的。我们绘制了一个填充线性渐变的大矩形,添加了阴影,然后在上面呈现文本。文字会渲染两次,以获得简单的阴影效果。

这种背景渲染相当昂贵,这就是视图启用QGraphicsView::CacheBackground 的原因。

void GraphWidget::scaleView(qreal scaleFactor)
{
    qreal factor = transform().scale(scaleFactor, scaleFactor).mapRect(QRectF(0, 0, 1, 1)).width();
    if (factor < 0.07 || factor > 100)
        return;

    scale(scaleFactor, scaleFactor);
}

scaleView() 辅助函数会检查缩放因子是否保持在一定范围内(即不能放大或缩小太多),然后将缩放因子应用到视图中。

main() 函数

与本示例其他部分的复杂性相比,main() 函数非常简单:我们先创建QApplication 实例,然后创建并显示GraphWidget 实例。由于网格中的所有节点最初都被移动,因此GraphWidget 定时器将在控制返回事件循环后立即启动。

示例项目 @ code.qt.io

© 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.