弹性节点示例
演示如何与场景中的图形元素进行交互。
“弹性节点”示例展示了如何在图中实现节点间的边,并支持基本交互。您可以点击并拖动节点,使用鼠标滚轮或键盘进行缩放。按下空格键将随机排列节点。该示例还具有分辨率独立性;即使放大,图形仍保持清晰。

Graphics View 提供了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(),以确保其击中区域呈椭圆形(而非默认的边界矩形)。
为了便于边的管理,该节点提供了一个简单的 API,用于向节点添加边以及列出所有连接的边。
每当场景状态前进一步时,都会调用advance() 的重写版本。此时会调用 calculateForces() 函数,以计算作用于该节点及其邻居节点上的推力和拉力。
Node 类还重写了itemChange()方法以响应状态变化(在此情况下为位置变化),并重写了mousePressEvent()和mouseReleaseEvent()方法以更新该项的视觉外观。
我们将从构造函数入手,开始审视 `Node ` 的实现:
Node::Node(GraphWidget *graphWidget)
: graph(graphWidget)
{
setFlag(ItemIsMovable);
setFlag(ItemSendsGeometryChanges);
setCacheMode(DeviceCoordinateCache);
setZValue(-1);
}在构造函数中,我们将ItemIsMovable 标志设为 true,以便项目能够响应鼠标拖动而移动;并将ItemSendsGeometryChanges 设为 true,以启用位置和变换变化时的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;
}节点之间的边代表将节点拉拢的力。通过遍历与该节点相连的每条边,我们可以采用与上述类似的方法来确定所有拉力的方向和大小。这些力将从xvel 和yvel 中减去。
理论上,推力和拉力的总和应精确稳定在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 的边界矩形是一个 20×20 大小的矩形,其中心位于原点 (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::yellow 到Qt::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 对比)。
构造函数接受两个节点指针作为输入。在此示例中,这两个指针均为必填参数。我们还为每个节点提供了相应的获取函数。
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;
}源和目标的 get-function 函数仅返回相应的指针。
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() 中,我们定义了两个点:sourcePoint 和destPoint ,它们分别指向源节点和目标节点的原点。每个点都是使用局部坐标计算得出的。
我们希望边箭头的尖端指向节点的精确轮廓,而不是节点的中心。为了找到这个点,我们首先将从源节点中心指向目标节点中心的向量分解为 X 和 Y 分量,然后通过除以向量的长度来归一化这些分量。 这样就得到了 X 和 Y 方向的单位偏移量,将其与节点的半径(即 10)相乘,即可得到需要加到边的一端并从另一端减去的偏移量。
如果向量长度小于 20(即两个节点重叠),则我们将源节点和目标节点的指针固定在源节点的中心。实际上,这种情况很难手动重现,因为此时两个节点之间的力会达到最大值。
需要注意的是,我们在该函数中调用了prepareGeometryChange()。原因是sourcePoint 和destPoint 这两个变量在绘制时会被直接使用,而它们正是由boundingRect()重写版本返回的。 为了保持 Graphics View 的内部管理整洁,在更改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 类定义
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 ,以缓存其静态且稍显复杂的背景渲染。由于图形渲染的是密集分布且不断移动的小对象集合,Graphics View 无需浪费时间寻找精确的更新区域,因此我们设置了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() 实现缩放,而回车键和空格键则用于随机化节点的位置。所有其他键盘事件(例如Page Up和Page Down)均由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 定时器将立即启动。
© 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.