碰撞老鼠示例
演示如何在图形视图中动画化项目。
碰撞老鼠示例展示了如何使用图形视口框架实现动画项目并检测项目之间的碰撞。
图形视口提供用于管理并交互大量自定义的基于 QGraphicsItem 类的 2D 图形项目(从 QGraphicsItem 类派生),以及一个支持缩放和旋转的 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 小部件。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 帧的帧率,对于大多数动画来说已经足够快。通过将定时器与 advance() 的一次连接来执行动画,可以确保所有鼠标同时移动,更重要的是,在所有鼠标移动后,只向屏幕发送一个更新。
© 2024 The Qt Company Ltd. 本文档中包含的贡献属于各自所有者的版权。提供的文档根据自由软件基金会公布的GNU自由文档许可证第1.3版进行许可。Qt和相应的标志是芬兰及全球其他地区的The Qt Company Ltd.的商标。所有其他商标均为其各自所有者的财产。