Auf dieser Seite

Beispiel „Kollidierende Mäuse“

Veranschaulicht, wie Elemente in einer Grafikansicht animiert werden.

Das Beispiel „Kollidierende Mäuse“ zeigt, wie man mithilfe des Graphics-View-Frameworks animierte Elemente implementiert und Kollisionen zwischen Elementen erkennt.

Anwendung mit Grafiken von Mäusen und Käse

„Graphics View“ stellt die Klasse „ QGraphicsScene “ zur Verwaltung und Interaktion mit einer großen Anzahl benutzerdefinierter 2D-Grafikelemente bereit, die von der Klasse „ QGraphicsItem “ abgeleitet sind, sowie das Widget „ QGraphicsView “ zur Visualisierung der Elemente mit Unterstützung für Zoomen und Drehen.

Das Beispiel besteht aus einer Elementklasse und einer Hauptfunktion: Die Klasse „ Mouse “ repräsentiert die einzelnen Mäuse, die von „ QGraphicsItem “ abgeleitet sind, und die Funktion „ main() “ stellt das Hauptanwendungsfenster bereit.

Zunächst werden wir die Klasse „ Mouse “ betrachten, um zu sehen, wie Elemente animiert und Kollisionen zwischen Elementen erkannt werden, und anschließend werden wir die Funktion „ main() “ untersuchen, um zu sehen, wie die Elemente in eine Szene eingefügt und die entsprechende Ansicht implementiert wird.

Definition der Klasse „Mouse“

Die Klasse „ mouse “ erbt von „ QGraphicsItem “. Die Klasse „ QGraphicsItem “ ist die Basisklasse für alle grafischen Elemente im Graphics View-Framework und bietet eine schlanke Grundlage für die Erstellung eigener benutzerdefinierter Elemente.

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

Beim Erstellen eines benutzerdefinierten Grafikelements müssen Sie die beiden rein virtuellen öffentlichen Funktionen von ` QGraphicsItem` implementieren: ` boundingRect()`, die eine Schätzung der vom Element gezeichneten Fläche zurückgibt, und ` paint()`, die das eigentliche Zeichnen implementiert. Darüber hinaus implementieren wir die Methoden ` shape()` und ` advance()` neu. Wir implementieren „ shape “() neu, um eine genaue Form unseres Maus-Elements zurückzugeben; die Standardimplementierung gibt lediglich das Begrenzungsrechteck des Elements zurück. Wir implementieren „ advance “() neu, um die Animation so zu handhaben, dass sie vollständig in einem einzigen Update abläuft.

Definition der Mausklasse

Beim Erstellen eines Maus-Elements stellen wir zunächst sicher, dass alle privaten Variablen des Elements, die nicht bereits direkt in der Klasse initialisiert wurden, ordnungsgemäß initialisiert werden:

Mouse::Mouse() : color(QRandomGenerator::global()->bounded(256),
                       QRandomGenerator::global()->bounded(256),
                       QRandomGenerator::global()->bounded(256))
{
    setRotation(QRandomGenerator::global()->bounded(360 * 16));
}

Zur Berechnung der verschiedenen Farbkomponenten der Maus verwenden wir ` QRandomGenerator`.

Anschließend rufen wir die Funktion ` setRotation()` auf, die von ` QGraphicsItem` geerbt wurde. Objekte befinden sich in ihrem eigenen lokalen Koordinatensystem. Ihre Koordinaten sind in der Regel um (0, 0) zentriert, und dies ist auch der Mittelpunkt für alle Transformationen. Durch den Aufruf der Funktion ` setRotation()` des Objekts ändern wir die Richtung, in die sich die Maus zu bewegen beginnt.

Wenn der „ QGraphicsScene “ beschließt, die Szene um einen Frame vorwärts zu bewegen, ruft er für jedes Element die Funktion „ QGraphicsItem::advance()“ auf. Dies ermöglicht es uns, unsere Maus mithilfe unserer Neuimplementierung der Funktion „advance()“ zu animieren.

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

Zunächst führen wir keinen Vorschub durch, wenn der Schritt 0 ist. Das liegt daran, dass advance() zweimal aufgerufen wird: einmal mit step == 0, was anzeigt, dass die Elemente kurz vor dem Vorschub stehen, und dann mit step == 1 für den eigentlichen Vorschub. Außerdem stellen wir sicher, dass die Maus innerhalb eines Kreises mit einem Radius von 150 Pixeln bleibt.

Beachten Sie die Funktion „ mapFromScene()“, die von „ QGraphicsItem “ bereitgestellt wird. Diese Funktion bildet eine in Szenenkoordinaten angegebene Position auf das Koordinatensystem des Elements ab.

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

Anschließend versuchen wir, Kollisionen mit anderen Mäusen zu vermeiden.

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

Schließlich berechnen wir die Geschwindigkeit der Maus und ihre Blickrichtung (zur Verwendung beim Zeichnen der Maus) und legen ihre neue Position fest.

Die Position eines Elements beschreibt seinen Ursprung (lokale Koordinate (0, 0)) im Koordinatensystem des übergeordneten Elements. Die Funktion ` QGraphicsItem::setPos()` setzt die Position des Elements auf die angegebene Position im Koordinatensystem des übergeordneten Elements. Bei Elementen ohne übergeordnetes Element wird die angegebene Position als Szenekoordinaten interpretiert. „ QGraphicsItem “ stellt außerdem die Funktion „ mapToParent()“ bereit, um eine in Elementkoordinaten angegebene Position auf das Koordinatensystem des übergeordneten Elements abzubilden. Hat das Element kein übergeordnetes Element, wird die Position stattdessen auf das Koordinatensystem der Szene abgebildet.

Nun ist es an der Zeit, eine Implementierung für die von ` QGraphicsItem` geerbten rein virtuellen Funktionen bereitzustellen. Werfen wir zunächst einen Blick auf die Funktion ` boundingRect()`:

QRectF Mouse::boundingRect() const
{
    qreal adjust = 0.5;
    return QRectF(-18 - adjust, -22 - adjust,
                  36 + adjust, 60 + adjust);
}

Die Funktion ` boundingRect()` definiert die äußeren Begrenzungen des Elements als Rechteck. Beachten Sie, dass das Graphics-View-Framework anhand des Begrenzungsrechtecks ermittelt, ob das Element neu gezeichnet werden muss; daher muss das gesamte Zeichnen innerhalb dieses Rechtecks erfolgen.

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

Das Graphics-View-Framework ruft die Funktion ` paint()` auf, um den Inhalt des Elements zu zeichnen; die Funktion zeichnet das Element in lokalen Koordinaten.

Beachten Sie die Darstellung der Ohren: Immer wenn ein Maus-Element mit anderen Maus-Elementen kollidiert, werden seine Ohren rot ausgefüllt; andernfalls sind sie dunkelgelb ausgefüllt. Wir verwenden die Funktion ` QGraphicsScene::collidingItems()``, um zu prüfen, ob es kollidierende Mauselemente gibt. Die eigentliche Kollisionserkennung wird vom Graphics-View-Framework mithilfe der Schnittmenge von Formen durchgeführt. Wir müssen lediglich sicherstellen, dass die Funktion ` QGraphicsItem::shape()`` eine genaue Form für unser Element zurückgibt:

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

Da die Komplexität der Schnittmenge zweier beliebiger Formen um eine Größenordnung zunimmt, wenn die Formen komplex sind, kann dieser Vorgang merklich zeitaufwendig sein. Ein alternativer Ansatz besteht darin, die Funktion ` collidesWithItem()` neu zu implementieren, um einen eigenen Algorithmus für die Kollision von Elementen und Formen bereitzustellen.

Damit ist die Implementierung der Klasse ` Mouse ` abgeschlossen; sie ist nun einsatzbereit. Werfen wir einen Blick auf die Funktion ` main() `, um zu sehen, wie eine Szene für die Mäuse und eine Ansicht zur Darstellung des Inhalts der Szene implementiert werden.

Die Funktion `Main()`

Die Funktion ` main() ` stellt das Hauptanwendungsfenster bereit und erstellt außerdem die Objekte, deren Szene sowie eine entsprechende Ansicht.

int main(int argc, char **argv)
{
    QApplication app(argc, argv);

Zunächst erstellen wir ein Anwendungsobjekt und die Szene:

    QGraphicsScene scene;
    scene.setSceneRect(-300, -300, 600, 600);

Die Klasse ` QGraphicsScene ` dient als Container für `QGraphicsItems`. Sie bietet außerdem Funktionen, mit denen Sie die Position von Elementen effizient bestimmen sowie feststellen können, welche Elemente innerhalb eines beliebigen Bereichs der Szene sichtbar sind.

Beim Erstellen einer Szene wird empfohlen, das Rechteck der Szene festzulegen; dieses Rechteck definiert die Ausdehnung der Szene. Es wird in erster Linie von ` QGraphicsView ` verwendet, um den standardmäßigen scrollbaren Bereich der Ansicht zu bestimmen, sowie von ` QGraphicsScene `, um die Indizierung der Elemente zu verwalten. Wird es nicht explizit festgelegt, entspricht das Standardrechteck der Szene dem größten umschließenden Rechteck aller Elemente in der Szene seit deren Erstellung. Das bedeutet, dass das Rechteck wächst, wenn Elemente zur Szene hinzugefügt oder darin verschoben werden, aber niemals schrumpft.

    scene.setItemIndexMethod(QGraphicsScene::NoIndex);

Die Elementindexfunktion dient dazu, das Auffinden von Elementen zu beschleunigen. „ NoIndex “ bedeutet, dass die Suche nach der Position eines Elements lineare Komplexität aufweist, da alle Elemente in der Szene durchsucht werden. Das Hinzufügen, Verschieben und Entfernen von Elementen erfolgt jedoch in konstanter Zeit. Dieser Ansatz eignet sich ideal für dynamische Szenen, in denen kontinuierlich viele Elemente hinzugefügt, verschoben oder entfernt werden. Die Alternative ist „ BspTreeIndex “, bei der eine binäre Suche genutzt wird, um Algorithmen zur Objektsuche zu erzielen, deren Komplexität näher an der logarithmischen Komplexität liegt.

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

Anschließend fügen wir die Mäuse zur Szene hinzu.

    QGraphicsView view(&scene);
    view.setRenderHint(QPainter::Antialiasing);
    view.setBackgroundBrush(QPixmap(":/images/cheese.jpg"));

Um die Szene anzeigen zu können, müssen wir außerdem ein ` QGraphicsView `-Widget erstellen. Die Klasse ` QGraphicsView ` visualisiert den Inhalt einer Szene in einem scrollbaren Ansichtsbereich. Wir stellen außerdem sicher, dass der Inhalt mit Antialiasing gerendert wird, und erstellen den Käse-Hintergrund, indem wir den Hintergrundpinsel der Ansicht festlegen.

Das für den Hintergrund verwendete Bild wird mithilfe des Qt-Ressourcensystems als Binärdatei in der ausführbaren Datei der Anwendung gespeichert. Der Konstruktor von ` QPixmap ` akzeptiert sowohl Dateinamen, die auf tatsächliche Dateien auf der Festplatte verweisen, als auch Dateinamen, die auf die in die Anwendung eingebetteten Ressourcen verweisen.

    view.setCacheMode(QGraphicsView::CacheBackground);
    view.setViewportUpdateMode(QGraphicsView::BoundingRectViewportUpdate);
    view.setDragMode(QGraphicsView::ScrollHandDrag);

Anschließend legen wir den Cache-Modus fest; ` QGraphicsView ` kann vorgerenderte Inhalte in einer Pixmap zwischenspeichern, die dann in den Ansichtsbereich gezeichnet wird. Der Zweck einer solchen Zwischenspeicherung besteht darin, die gesamte Renderzeit für Bereiche zu beschleunigen, deren Darstellung viel Zeit in Anspruch nimmt, zum Beispiel: Texturen, Farbverläufe und Hintergründe mit Alpha-Mischung. Die Eigenschaft „ CacheMode “ legt fest, welche Teile der Ansicht zwischengespeichert werden, und das Flag „ CacheBackground “ aktiviert die Zwischenspeicherung des Hintergrunds der Ansicht.

Durch das Setzen der Eigenschaft „ dragMode “ legen wir fest, was geschehen soll, wenn der Benutzer auf den Szenenhintergrund klickt und die Maus zieht. Das Flag „ ScrollHandDrag “ bewirkt, dass sich der Cursor in eine zeigende Hand verwandelt, und das Ziehen der Maus bewirkt das Scrollen der Bildlaufleisten.

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

Abschließend legen wir den Titel und die Größe des Anwendungsfensters fest, bevor wir mit der Funktion ` QApplication::exec()` in die Haupt-Ereignisschleife eintreten.

Zuletzt erstellen wir einen ` QTimer ` und verbinden dessen `timeout()`-Signal mit dem `advance()`-Slot der Szene. Jedes Mal, wenn der Timer ausgelöst wird, wird die Szene um ein Bild weitergeschaltet.

Anschließend weisen wir den Timer an, alle 1000/33 Millisekunden auszulösen. Dadurch erhalten wir eine Bildrate von 30 Bildern pro Sekunde, was für die meisten Animationen schnell genug ist. Die Animation mit einer einzigen Timer-Verbindung zum Weiterleiten der Szene stellt sicher, dass alle Mäuse gleichzeitig bewegt werden und – was noch wichtiger ist – erst dann eine Aktualisierung an den Bildschirm gesendet wird, wenn sich alle Mäuse bewegt haben.

Beispielprojekt @ 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.