本页内容

鼠标碰撞示例

演示如何在图形视图上对对象进行动画处理。

“鼠标碰撞”示例展示了如何使用图形视图框架来实现动画元素并检测元素之间的碰撞。

运行时显示老鼠和奶酪图形的应用程序

Graphics View 提供了 `QGraphicsScene ` 类,用于管理和与大量从 `QGraphicsItem ` 类派生的自定义 2D 图形对象进行交互;同时还提供了一个 `QGraphicsView ` 控件,用于可视化这些对象,并支持缩放和旋转。

该示例由一个项类和一个主函数组成:Mouse 类表示继承自QGraphicsItem 类的各个鼠标,而main() 函数则提供主应用程序窗口。

我们将首先回顾Mouse 类,了解如何对项目进行动画处理以及检测项目碰撞;然后我们将回顾main() 函数,了解如何将项目放入场景以及如何实现相应的视图。

Mouse 类的定义

mouse 类继承自QGraphicsItem 类。QGraphicsItem 类是Graphics View框架中所有图形项的基类,为编写自定义图形项提供了轻量级的基础。

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::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() 函数将项的外界限定义为一个矩形。请注意,Graphics View 框架会使用该边界矩形来判断项是否需要重绘,因此所有绘制操作都必须在此矩形内部进行。

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);
}

Graphics View 框架会调用paint() 函数来绘制该项的内容;该函数在本地坐标系中绘制该项。

请注意耳朵的绘制:每当一个鼠标项与其他鼠标项发生碰撞时,其耳朵会被填充为红色;否则则填充为深黄色。 我们使用QGraphicsScene::collidingItems()函数来检查是否有任何鼠标项发生碰撞。实际的碰撞检测由Graphics View框架通过形状-形状交集来处理。我们只需确保QGraphicsItem::shape()函数为我们的项返回一个准确的形状:

QPainterPath Mouse::shape() const
{
    QPainterPath path;
    path.addRect(-10, -20, 20, 40);
    return path;
}

由于当形状复杂时,任意形状与形状的相交运算复杂度会呈数量级增长,因此该操作可能耗时较长。另一种方法是重写collidesWithItem() 函数,以提供自定义的项目与形状碰撞算法。

至此,Mouse 类的实现已完成,现已可以投入使用。接下来,让我们看看main() 函数,了解如何实现一个用于鼠标的场景以及一个用于显示场景内容的视图。

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 可以将预渲染的内容缓存到像素图中,然后将其绘制到视口上。 此类缓存的目的是加快渲染速度较慢区域的总渲染时间,例如:纹理、渐变以及带透明度混合的背景。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 帧的帧率,对于大多数动画来说已经足够快。通过单个计时器连接来驱动场景前进,可以确保所有老鼠在同一时刻移动,更重要的是,在所有老鼠都移动完毕后,屏幕上只会发送一次更新。

示例项目 @ code.qt.io

© 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.