弹性节点示例

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

弹性节点示例展示了如何在图中的节点之间实现带基本交互的边缘。您可以点击并拖动节点,使用鼠标滚轮或键盘进行缩放。按下空格键将随机化节点。该示例也是分辨率无关的;当你缩放时,图形仍然清晰。

图形视图提供了用于管理并与之交互的从 QGraphicsScene 类派生的许多自定义 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() 确保其击中区域具有椭圆形(与默认边界矩形相反)。

为了边管理,节点提供了一个简单的 API 来向节点添加边,并列出所有连接的边。

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()函数实现了弹性效果,该效果在网格中拉着节点推拉。此外,用户可以直接使用鼠标拖动其中一个节点。因为我们不希望两种方法同时作用在同一个节点上,所以我们首先检查这个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;
        }
    }

“弹性”效果来自于一个应用推拉力的算法。效果很令人印象深刻,而且 surprisingly simplest to implement。

该算法有两个步骤:第一步是计算推动节点分开的力,第二步是减去拉引节点在一起的力。首先,我们需要找到图中的所有节点。我们通过调用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 的边界矩形是一个以原点 (0, 0) 为中心、大小为 20x20 的矩形,所有方向上调整 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 个单位的位置绘制一个简单的深灰色椭圆投影。

然后,我们绘制一个具有径向渐变的椭圆。当提升时,填充颜色从 Qt::yellowQt::darkYellow,下沉时相反。在下沉状态下,我们也将中心和焦点向下和向右移动 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;
};

EdgeQGraphicsItem 继承,因为它是没有使用信号、槽和属性的简单类(与 QGraphicsObject 对比)。

构造函数接受两个节点指针作为输入。在这个例子中,两个指针都是必需的。我们还为每个节点提供了获取方法。

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(),以保持图形视图内部记账的整洁。最安全的方法是在任何这样的变量修改之前立即调用此函数一次。

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,如果是,则也返回。

    // 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 类定义

GraphWidgetQGraphicsView的子类,为主窗口提供滚动条。

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:
    int timerId = 0;
    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。然后场景得到一个固定的场景矩形,并分配给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()
{
    if (!timerId)
        timerId = startTimer(1000 / 25);
}

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()放大和缩小,enter和space键随机化节点的位置。所有其他按键事件(例如,向上和向下翻页)都由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) {
        killTimer(timerId);
        timerId = 0;
    }
}

计时器事件处理器的任务是作为平滑动画运行整个力计算机制。每次计时器触发时,处理器将找到场景中的所有节点,并对每个节点逐个调用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() 非常简单:我们创建了一个 QApplication 实例,然后创建并显示了 GraphWidget 实例。由于网格中的所有节点最初都进行了移动,因此当控制权返回到事件循环后,GraphWidget 的计时器将立即启动。

示例项目 @ code.qt.io

© 2024 Qt公司有限公司。本文档中包含的贡献文档版权属于各自的所有者。本提供的文档是根据自由软件基金会发布的 GNU自由文档许可版本1.3 许可的。Qt及其相应的商标是芬兰及/或其他国家和地区的Qt公司有限公司的商标。所有其他商标均为各自所有者的财产。