以下文字翻译自Qt官方自带例程说明,并加入了自己的理解与说明。
弹性节点示例演示如何使用基本交互在图形中实现节点之间的边。您可以单击以拖动节点,并使用鼠标滚轮或键盘放大和缩小。点击空格键将随机化节点。该示例也是独立于分辨率的;放大时,图形仍然清晰。
图形视图提供了 QGraphicsScene 类,用于管理和与从 QGraphicsItem 类派生的大量自定义 2D 图形项进行交互,并提供一个 QGraphicsView 组件来可视化这些项,并支持缩放和旋转。
此示例由 Node 类、Edge 类、GraphWidget 测试和 main 函数组成:Node 类表示网格中可拖动的黄色节点,Edge 类表示节点之间的线条,GraphWidget 类表示应用程序窗口,main() 函数创建并显示此窗口,并运行事件循环。
Node类定义
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 advance();
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,用于向节点添加边缘以及列出所有连接的边缘,即addEdge()函数。
每当场景的状态前进一步时,就会调用 advance() 重新实现。调用 calculateForces() 函数来计算在此节点及其相邻节点上推拉的力。实际应用中,一般都通过 advance()完成item类动画效果的展示。
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。
setFlag函数:设置item类的某些标志位,相当于启动了某些功能。
ItemIsMovable标志位:该item支持使用鼠标进行交互式移动。详情可看【Qt系列】Graphics View框架下实现自定义item类的拖动
ItemSendsGeometryChanges标志位:启动后,item几何位置改变会发出通知。详情可看【Qt系列】QGraphicsItem状态改变函数itemChange之pos改变发送通知示例
setCacheMode函数:设置item的缓存模式。
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() 函数实现了在网格中的节点上拉动和推入的弹性效果。节点的移动有两种形式,一种是由鼠标拖拽移动,一种是被牵扯移动。上述代码判断是否是当前的鼠标抓取器项目(即QGraphicsScene::mouseGrabberItem()),是的话新位置直接跟随鼠标位置,不用去计算力,直接return。如果不是,则需要综合所有相邻(但不一定连接)节点,计算合力,确定方向。
mouseGrabberItem()函数说明:
返回当前鼠标抓取器项,如果当前没有项正在抓取鼠标,则返回 0。鼠标抓取器项是接收发送到场景的所有鼠标事件的项。
当项目接收并接受鼠标按下事件时,它将成为鼠标抓取器,并且它会一直保持鼠标抓取,直到发生以下任一事件:
1.如果项目在未按下其他按钮时收到鼠标释放事件,则将失去鼠标抓取。
2.如果项目变得不可见(即,有人调用 item->setVisible(false)),或者如果它被禁用(即,有人调用 item->setEnabled(false)),它将失去鼠标抓取。
3.如果从场景中移除该项目,则会丢失鼠标抓取。如果项目失去鼠标抓取,场景将忽略所有鼠标事件,直到新项目抓住鼠标(即,直到新项目收到鼠标按下事件)。
简单来说就是,返回此时鼠标正在抓取的item指针。
// 对推动该节点的所有力进行求和
qreal xvel = 0;
qreal yvel = 0;
foreach (QGraphicsItem *item, scene()->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 实例并进行类型转换,即去掉边Item。
详情可看【Qt系列】QGraphicsItem的类型检测与转换
我们使用mapFromItem()创建一个临时矢量,从this节点指向另一个节点,在本地坐标中。我们使用此矢量的分解分量来确定应用于this节点的力的方向和强度。力为每个节点累积,然后进行调整,以便为最近的节点提供最强的力,当距离增加时会迅速退化。所有力的总和以xvel(X速度)和yvel(Y速度)存储。
mapFromItem()函数返回的是node节点在this节点坐标系下的坐标点QPointF。
// 计算所有拉力之和
double weight = (edgeList.size() + 1) * 10;
foreach (Edge *edge, edgeList) {
QPointF vec;
if (edge->sourceNode() == this) //如果该edge的源节点是this节点
vec = mapToItem(edge->destNode(), 0, 0);
else
vec = mapToItem(edge->sourceNode(), 0, 0);
xvel -= vec.x() / weight;
yvel -= vec.y() / weight;
}
节点之间的边表示将节点拉在一起的力。通过访问连接到此节点的每个边,我们可以使用与上面类似的方法来找到所有拉力的方向和强度。这些力从 xvel 和 yvel 中减去。
总结:推力是针对其他任何节点的,拉力是针对相连的节点的。
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() 的最后一步确定节点的新位置。我们将力添加到节点的当前位置。我们还确保新位置保持在我们定义的边界内。我们实际上并没有在这个函数中移动item,只是更新了newPos;这是在一个单独的步骤advance()中完成的。
bool Node::advance()
{
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);
}
节点的边界矩形是一个 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).light(120));
gradient.setColorAt(0, QColor(Qt::darkYellow).light(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) 处绘制一个简单的深灰色椭圆投影,即 (3, 3) 个单位,从椭圆的左上角 (-10, -10) 向右移动。
然后,我们绘制一个带有径向渐变填充的椭圆。此填充物在升高时为 黄色到灰黄色,在下沉时为相反的填充物。在下沉状态下,我们还将中心和焦点移位(3,3),以强调某些东西被推倒的印象。
绘制具有渐变的填充椭圆可能非常慢,尤其是在使用复杂渐变(如 QRadialGradient)时。这就是此示例使用 DeviceCoordinateCache 的原因,这是一种简单而有效的措施,可防止不必要的重绘。
QVariant Node::itemChange(GraphicsItemChange change, const QVariant &value)
{
switch (change) {
case ItemPositionHasChanged:
foreach (Edge *edge, edgeList)
edge->adjust();
graph->itemMoved();
break;
default:
break;
};
return QGraphicsItem::itemChange(change, value);
}
我们重新实现 itemChange() 以调整所有连接边缘的位置,并通知场景某个项目已移动(即"发生了一些事情")。这将触发新的力计算。即当某个节点移动时,其他Item就该重新计算力。
此通知是节点需要保留指针返回 GraphWidget 的唯一原因。另一种办法是使用信号提供这种通知。在这种情况下,Node 需要从 QGraphicsObject 继承。
void Node::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
update();
QGraphicsItem::mousePressEvent(event);
}
void Node::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
{
update();
QGraphicsItem::mouseReleaseEvent(event);
}
由于我们已经设置了 ItemIsMovable 标志,因此我们不需要实现根据鼠标输入移动节点的逻辑。但是,我们仍然需要重新实现鼠标按下和释放处理程序,以更新节点的视觉外观(即,凹陷或凸起)。按下或者释放都调用update函数重绘节点,即调用paint函数。
Edge 类定义
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;
};
Edge继承自QGraphicsItem,因为它是一个简单的类,没有使用信号,槽和属性(与QGraphicsObject相比)。
构造函数采用两个节点指针作为输入。在此示例中,这两个指针都是必需的。我们还为每个节点提供 get 函数。
adjust() 函数重新定位边缘,并且该项目还实现了 boundingRect() 和 paint()。
我们现在看看其构造函数。
Edge::Edge(Node *sourceNode, Node *destNode)
: arrowSize(10)
{
setAcceptedMouseButtons(0);
source = sourceNode;
dest = destNode;
source->addEdge(this);
dest->addEdge(this);
adjust();
}
Edge 构造函数将其箭头大小数据成员初始化为 10 个单位;这决定了在 paint() 中绘制的箭头的大小。
在构造函数体中,我们调用 setAcceptedMouseButtons(0)。这可确保根本不会将边缘项视为鼠标输入(即您无法单击边缘)。然后更新源指针和目标指针,将此边注册到每个节点,我们调用 adjust() 来更新此边的起点结束位置。
Node *Edge::sourceNode() const
{
return source;
}
Node *Edge::destNode() const
{
return dest;
}
源函数和目标 get 函数仅返回各自的指针。
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() 重新实现中返回的。在更改 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);
}
边的边界矩形定义为包含边的起点和终点的最小矩形。由于我们在每条边上绘制一个箭头,因此我们还需要通过在其方向上调整一半的箭头大小和一半的笔宽来进行补偿。笔用于绘制箭头的轮廓,我们可以假设轮廓的一半可以在箭头区域之外绘制,一半将在内部绘制。asjusted函数就是在现有矩阵上扩充一点。
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 = ::acos(line.dx() / line.length());
if (line.dy() >= 0)
angle = TwoPi - angle;
QPointF sourceArrowP1 = sourcePoint + QPointF(sin(angle + Pi / 3) * arrowSize,
cos(angle + Pi / 3) * arrowSize);
QPointF sourceArrowP2 = sourcePoint + QPointF(sin(angle + Pi - Pi / 3) * arrowSize,
cos(angle + Pi - Pi / 3) * arrowSize);
QPointF destArrowP1 = destPoint + QPointF(sin(angle - Pi / 3) * arrowSize,
cos(angle - Pi / 3) * arrowSize);
QPointF destArrowP2 = destPoint + QPointF(sin(angle - Pi + Pi / 3) * arrowSize,
cos(angle - Pi + Pi / 3) * arrowSize);
painter->setBrush(Qt::black);
painter->drawPolygon(QPolygonF() << line.p1() << sourceArrowP1 << sourceArrowP2);
painter->drawPolygon(QPolygonF() << line.p2() << destArrowP1 << destArrowP2);
}
我们继续在边缘的两端绘制一个箭头。每个箭头都绘制为带有黑色填充的多边形。箭头的坐标使用简单的三角学确定。
自定义View类
GraphWidget是QGraphicsView的子类,它为主窗口提供了滚动条。
class GraphWidget : public QGraphicsView
{
Q_OBJECT
public:
GraphWidget(QWidget *parent = 0);
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;
Node *centerNode;
};
该类提供了一个初始化场景的基本构造函数、一个用于通知场景节点图中更改的 itemMoved() 函数、几个事件处理程序、一个重新实现的 drawBackground() 以及一个用于使用鼠标滚轮或键盘缩放视图的帮助器函数。
GraphWidget::GraphWidget(QWidget *parent)
: QGraphicsView(parent), timerId(0)
{
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的构造函数创建scene,并且由于大多数项目在大多数时间都会移动,因此它会设置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() 进行放大和缩小,输入键和空格键随机化节点的位置。所有其他关键事件(例如,向上翻页和向下翻页)都由 QGraphicsView 的默认实现处理。
void GraphWidget::timerEvent(QTimerEvent *event)
{
Q_UNUSED(event);
QList<Node *> nodes;
foreach (QGraphicsItem *item, scene()->items()) {
if (Node *node = qgraphicsitem_cast<Node *>(item))
nodes << node;
}
foreach (Node *node, nodes)
node->calculateForces();
bool itemsMoved = false;
foreach (Node *node, nodes) {
if (node->advance())
itemsMoved = true;
}
if (!itemsMoved) {
killTimer(timerId);
timerId = 0;
}
}
计时器事件处理程序的工作是将整个力计算机制作为平滑动画运行。每次触发计时器时,处理程序将查找场景中的所有节点,并在每个节点上调用 Node::calculateForces(),一次调用一个。然后,在最后一步中,它将调用 Node::advance() 将所有节点移动到其新位置。通过检查advance()的返回值,我们可以确定网格是否稳定(即没有节点移动)。如果是这样,我们可以停止计时器。
void GraphWidget::wheelEvent(QWheelEvent *event)
{
scaleView(pow((double)2, -event->delta() / 240.0));
}
在滚轮事件处理程序中,我们将鼠标滚轮增量转换为比例因子,并将此因子传递给 scaleView()。这种方法考虑了车轮滚动的速度。鼠标滚轮滚动得越快,视图缩放的速度就越快。
void GraphWidget::drawBackground(QPainter *painter, const QRectF &rect)
{
Q_UNUSED(rect);
// 阴影部分
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);
//填充
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);
// 文本
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::d rawBackground() 的重新实现中呈现。我们绘制一个填充有线性渐变的大矩形,添加一个投影,然后在顶部呈现文本。文本将呈现两次,以获得简单的投影效果。
这种背景渲染非常昂贵;这就是视图启用 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 实例,使用 qsrand() 为随机化器设定种子,然后创建并显示 GraphWidget 的实例。由于网格中的所有节点最初都是移动的,因此 GraphWidget 计时器将在控制返回到事件循环后立即启动。