碰撞小鼠示例
演示如何为图形视图上的项目制作动画。
Colliding Mice 示例展示了如何使用图形视图框架实现动画项目并检测项目间的碰撞。
图形视图提供了QGraphicsScene 类,用于管理大量源自QGraphicsItem 类的自定义 2D 图形项目并与之交互,还提供了QGraphicsView 部件,用于可视化项目,并支持缩放和旋转。
该示例由一个项目类和一个主函数组成:Mouse
类代表扩展了QGraphicsItem 的各个小鼠,而main()
函数提供了主应用程序窗口。
我们将首先回顾Mouse
类,了解如何为项目制作动画和检测项目碰撞,然后回顾main()
函数,了解如何将项目放入场景以及如何实现相应的视图。
鼠标类定义
mouse
类继承自QGraphicsItem 。QGraphicsItem 类是图形视图框架中所有图形项目的基类,为编写自定义项目提供了轻量级基础。
class Mouse : public QGraphicsItem { public: Mouse(); QRectF boundingRect() const override; QPainterPath shape() const override; void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; protected: void advance(int step) override; private: qreal angle = 0; qreal speed = 0; qreal mouseEyeDirection = 0; QColor color; };
编写自定义图形项目时,必须实现QGraphicsItem 的两个纯虚拟公共函数:boundingRect() 和paint() ,前者返回项目绘制区域的估计值,后者实现实际绘制。此外,我们还重新实现了shape() 和advance()。我们重新实现了shape() 以返回鼠标项的精确形状;默认实现只是返回项的边界矩形。我们重新实现advance() 来处理动画,这样所有动画都会在一次更新中完成。
鼠标类定义
在构建鼠标项时,我们首先要确保所有尚未在类中直接初始化的项的私有变量都已正确初始化:
Mouse::Mouse() : color(QRandomGenerator::global()->bounded(256), QRandomGenerator::global()->bounded(256), QRandomGenerator::global()->bounded(256)) { setRotation(QRandomGenerator::global()->bounded(360 * 16)); }
为了计算鼠标颜色的各个组成部分,我们使用QRandomGenerator 。
然后,我们调用从QGraphicsItem 继承而来的setRotation() 函数。 项目生活在自己的本地坐标系中。它们的坐标通常以 (0, 0) 为中心,这也是所有变换的中心。通过调用项目的setRotation() 函数,我们可以改变鼠标开始移动的方向。
当QGraphicsScene 决定将场景推进一帧时,它将调用每个项目的QGraphicsItem::advance() 函数。这样,我们就可以使用重新实现的 advance() 函数为鼠标制作动画。
void Mouse::advance(int step) { if (!step) return; QLineF lineToCenter(QPointF(0, 0), mapFromScene(0, 0)); if (lineToCenter.length() > 150) { qreal angleToCenter = std::atan2(lineToCenter.dy(), lineToCenter.dx()); angleToCenter = normalizeAngle((Pi - angleToCenter) + Pi / 2); if (angleToCenter < Pi && angleToCenter > Pi / 4) { // Rotate left angle += (angle < -Pi / 2) ? 0.25 : -0.25; } else if (angleToCenter >= Pi && angleToCenter < (Pi + Pi / 2 + Pi / 4)) { // Rotate right angle += (angle < Pi / 2) ? 0.25 : -0.25; } } else if (::sin(angle) < 0) { angle += 0.25; } else if (::sin(angle) > 0) { angle -= 0.25; }
首先,如果步长为0
,我们就不必进行任何前进操作。这是因为 advance() 会被调用两次:一次是 step ==0
时,表示项目即将前进;另一次是 step ==1
时,表示实际前进。我们还确保鼠标停留在半径为 150 像素的圆内。
请注意QGraphicsItem 提供的mapFromScene() 函数。该函数将场景坐标中给定的位置映射到项目坐标系中。
const QList<QGraphicsItem *> dangerMice = scene()->items(QPolygonF() << mapToScene(0, 0) << mapToScene(-30, -50) << mapToScene(30, -50)); for (const QGraphicsItem *item : dangerMice) { if (item == this) continue; QLineF lineToMouse(QPointF(0, 0), mapFromItem(item, 0, 0)); qreal angleToMouse = std::atan2(lineToMouse.dy(), lineToMouse.dx()); angleToMouse = normalizeAngle((Pi - angleToMouse) + Pi / 2); if (angleToMouse >= 0 && angleToMouse < Pi / 2) { // Rotate right angle += 0.5; } else if (angleToMouse <= TwoPi && angleToMouse > (TwoPi - Pi / 2)) { // Rotate left angle -= 0.5; } } if (dangerMice.size() > 1 && QRandomGenerator::global()->bounded(10) == 0) { if (QRandomGenerator::global()->bounded(1)) angle += QRandomGenerator::global()->bounded(1 / 500.0); else angle -= QRandomGenerator::global()->bounded(1 / 500.0); }
然后,我们会尽量避免与其他鼠标相撞。
speed += (-50 + QRandomGenerator::global()->bounded(100)) / 100.0; qreal dx = ::sin(angle) * 10; mouseEyeDirection = (qAbs(dx / 5) < 1) ? 0 : dx / 5; setRotation(rotation() + dx); setPos(mapToParent(0, -(3 + sin(speed) * 3))); }
最后,我们会计算鼠标的速度和眼球方向(用于绘制鼠标时),并设置其新位置。
项目的位置描述了它在父坐标系中的原点(局部坐标(0,0))。QGraphicsItem::setPos() 函数会将项目的位置设置为父坐标系中的给定位置。对于没有父对象的项目,给定位置被解释为场景坐标。QGraphicsItem 还提供了一个mapToParent() 函数,用于将项目坐标中的给定位置映射到父对象的坐标系中。如果项目没有父对象,位置将被映射到场景坐标系。
接下来就该为继承自QGraphicsItem 的纯虚拟函数提供实现方法了。让我们先看看boundingRect() 函数:
QRectF Mouse::boundingRect() const { qreal adjust = 0.5; return QRectF(-18 - adjust, -22 - adjust, 36 + adjust, 60 + adjust); }
boundingRect() 函数将项目的外部边界定义为矩形。请注意,图形视图框架使用边界矩形来确定项目是否需要重绘,因此所有绘制都必须在该矩形内完成。
void Mouse::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) { // Body painter->setBrush(color); painter->drawEllipse(-10, -20, 20, 40); // Eyes painter->setBrush(Qt::white); painter->drawEllipse(-10, -17, 8, 8); painter->drawEllipse(2, -17, 8, 8); // Nose painter->setBrush(Qt::black); painter->drawEllipse(QRectF(-2, -22, 4, 4)); // Pupils painter->drawEllipse(QRectF(-8.0 + mouseEyeDirection, -17, 4, 4)); painter->drawEllipse(QRectF(4.0 + mouseEyeDirection, -17, 4, 4)); // Ears painter->setBrush(scene()->collidingItems(this).isEmpty() ? Qt::darkYellow : Qt::red); painter->drawEllipse(-17, -12, 16, 16); painter->drawEllipse(1, -12, 16, 16); // Tail QPainterPath path(QPointF(0, 20)); path.cubicTo(-5, 22, -5, 22, 0, 25); path.cubicTo(5, 27, 5, 32, 0, 30); path.cubicTo(-5, 32, -5, 42, 0, 35); painter->setBrush(Qt::NoBrush); painter->drawPath(path); }
图形视图框架会调用paint() 函数来绘制项目的内容;该函数以本地坐标绘制项目。
请注意耳朵的绘制:当鼠标项与其他鼠标项发生碰撞时,它的耳朵就会被填充为红色;否则就会被填充为深黄色。我们使用QGraphicsScene::collidingItems() 函数来检查是否有碰撞的小鼠。实际的碰撞检测是由图形视图框架使用形状-形状交叉来处理的。我们要做的就是确保QGraphicsItem::shape() 函数为我们的项目返回准确的形状:
QPainterPath Mouse::shape() const { QPainterPath path; path.addRect(-10, -20, 20, 40); return path; }
当形状复杂时,任意形状-形状交集的复杂度会以数量级增长,因此这一操作会非常耗时。另一种方法是重新实现collidesWithItem() 函数,提供自己定制的项目和形状碰撞算法。
至此,Mouse
类的实现就完成了;现在就可以使用了。让我们看看main()
函数,了解如何为小鼠实现一个场景和一个显示场景内容的视图。
主()函数
main()
函数提供了主应用程序窗口,并创建了项目、其场景和相应的视图。
int main(int argc, char **argv) { QApplication app(argc, argv);
首先,我们创建一个应用程序对象并创建场景:
QGraphicsScene scene; scene.setSceneRect(-300, -300, 600, 600);
QGraphicsScene 类是 QGraphicsItems 的容器。它还提供了一些功能,可让您有效地确定项目的位置,以及确定在场景的任意区域内哪些项目是可见的。
创建场景时,建议设置场景的矩形,即定义场景范围的矩形。它主要由QGraphicsView 用来确定视图的默认可滚动区域,并由QGraphicsScene 用来管理项目索引。如果没有明确设置,场景的默认矩形将是自创建场景以来场景中所有项目的最大边界矩形。这意味着,当场景中添加或移动项目时,矩形会增大,但绝不会缩小。
scene.setItemIndexMethod(QGraphicsScene::NoIndex);
NoIndex 意味着项目定位具有线性复杂性,因为场景中的所有项目都会被搜索到。但是,添加、移动和删除项目则需要恒定的时间。这种方法非常适合动态场景,在这种场景中,许多物品会不断添加、移动或移除。另一种方法是BspTreeIndex ,它利用二进制搜索来实现项目定位算法,其复杂度更接近对数。
for (int i = 0; i < MouseCount; ++i) { Mouse *mouse = new Mouse; mouse->setPos(::sin((i * 6.28) / MouseCount) * 200, ::cos((i * 6.28) / MouseCount) * 200); scene.addItem(mouse); }
然后,我们将小鼠添加到场景中。
QGraphicsView view(&scene); view.setRenderHint(QPainter::Antialiasing); view.setBackgroundBrush(QPixmap(":/images/cheese.jpg"));
为了能够查看场景,我们还必须创建一个QGraphicsView widget。QGraphicsView 类可在一个可滚动的视口中显示场景的内容。我们还要确保使用抗锯齿技术渲染内容,并通过设置视图的背景刷来创建奶酪背景。
使用 Qt 的资源系统将背景图像作为二进制文件存储在应用程序的可执行文件中。QPixmap 构造函数既接受指向磁盘上实际文件的文件名,也接受指向应用程序嵌入资源的文件名。
view.setCacheMode(QGraphicsView::CacheBackground); view.setViewportUpdateMode(QGraphicsView::BoundingRectViewportUpdate); view.setDragMode(QGraphicsView::ScrollHandDrag);
然后,我们设置缓存模式;QGraphicsView 可以在像素图中缓存预渲染内容,然后将其绘制到视口上。这种缓存的目的是加快渲染速度较慢区域的总渲染时间,例如:纹理、渐变和 Alpha 混合背景。CacheMode 属性用于缓存视图的哪些部分,而CacheBackground 标志则用于缓存视图的背景。
通过设置dragMode 属性,我们定义了当用户点击场景背景并拖动鼠标时应发生的操作。ScrollHandDrag 标志使光标变成指向手,拖动鼠标将滚动滚动条。
view.setWindowTitle(QT_TRANSLATE_NOOP(QGraphicsView, "Colliding Mice")); view.resize(400, 300); view.show(); QTimer timer; QObject::connect(&timer, &QTimer::timeout, &scene, &QGraphicsScene::advance); timer.start(1000 / 33); return app.exec(); }
最后,在进入主事件循环之前,我们使用QApplication::exec() 函数设置应用程序窗口的标题和大小。
最后,我们创建了一个QTimer ,并将其 timeout() 信号连接到场景的 advance() 槽。计时器每触发一次,场景就前进一帧。
然后,我们告诉计时器每 1000/33 毫秒触发一次。这样我们就能获得每秒 30 帧的帧频,这对于大多数动画来说已经足够快了。使用单个定时器连接来推进场景,可确保所有小鼠在一个点上移动,更重要的是,在所有小鼠移动后,只需向屏幕发送一次更新。
© 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.