Beispiel für kollidierende Mäuse
Zeigt, wie man Elemente in einer Grafikansicht animiert.
Das Colliding Mice-Beispiel zeigt, wie man das Graphics View-Framework verwendet, um animierte Elemente zu implementieren und Kollisionen zwischen Elementen zu erkennen.
Graphics View bietet die Klasse QGraphicsScene für die Verwaltung und Interaktion mit einer großen Anzahl von benutzerdefinierten 2D-Grafikelementen, die von der Klasse QGraphicsItem abgeleitet sind, und ein QGraphicsView Widget für die 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 QGraphicsItem erweitern, und die Funktion main()
stellt das Hauptanwendungsfenster bereit.
Wir werden zuerst die Klasse Mouse
betrachten, um zu sehen, wie man Elemente animiert und Elementkollisionen erkennt, und dann werden wir die Funktion main()
betrachten, um zu sehen, wie man die Elemente in eine Szene einfügt und wie man die entsprechende Ansicht implementiert.
Definition der Mausklasse
Die Klasse mouse
erbt von QGraphicsItem. Die Klasse QGraphicsItem ist die Basisklasse für alle grafischen Elemente im Graphics View Framework und bietet eine leichtgewichtige Grundlage für das Schreiben 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; };
Wenn Sie ein benutzerdefiniertes Grafikelement schreiben, müssen Sie die beiden rein virtuellen öffentlichen Funktionen von QGraphicsItem implementieren: boundingRect(), die eine Schätzung der vom Element gemalten Fläche zurückgibt, und paint(), die das eigentliche Malen implementiert. Außerdem müssen wir die Funktionen shape() und advance() neu implementieren. Wir reimplementieren shape(), um eine genaue Form unseres Mausobjekts zurückzugeben; die Standardimplementierung gibt einfach das begrenzende Rechteck des Objekts zurück. Wir reimplementieren advance(), um die Animation so zu handhaben, dass alles in einem Update geschieht.
Definition der Mausklasse
Bei der Konstruktion eines Mausobjekts stellen wir zunächst sicher, dass alle privaten Variablen des Objekts, die noch nicht direkt in der Klasse initialisiert wurden, richtig initialisiert sind:
Mouse::Mouse() : color(QRandomGenerator::global()->bounded(256), QRandomGenerator::global()->bounded(256), QRandomGenerator::global()->bounded(256)) { setRotation(QRandomGenerator::global()->bounded(360 * 16)); }
Um die verschiedenen Komponenten der Mausfarbe zu berechnen, verwenden wir QRandomGenerator.
Dann rufen wir die Funktion setRotation() auf, die von QGraphicsItem geerbt wurde. Elemente leben in ihrem eigenen lokalen Koordinatensystem. Ihre Koordinaten sind normalerweise um (0, 0) zentriert, und dies ist auch das Zentrum für alle Transformationen. Durch den Aufruf der Funktion setRotation() des Elements ändern wir die Richtung, in die sich die Maus zu bewegen beginnt.
Wenn QGraphicsScene beschließt, die Szene um ein Bild zu verschieben, ruft es QGraphicsItem::advance() für jedes Element auf. So können wir unsere Maus mit unserer Neuimplementierung der Funktion advance() 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 machen wir uns nicht die Mühe, einen Schritt vorwärts zu machen, wenn der Schritt 0
ist. Der Grund dafür ist, dass advance() zweimal aufgerufen wird: einmal mit step == 0
, was anzeigt, dass sich die Elemente bewegen werden, und dann mit step == 1
für die tatsächliche Bewegung. Wir stellen auch 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 ordnet eine in Szenekoordinaten angegebene Position dem Koordinatensystem des Objekts zu.
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); }
Dann 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 Malen der Maus) und setzen ihre neue Position.
Die Position eines Elements beschreibt seinen Ursprung (lokale Koordinate (0, 0)) in den übergeordneten Koordinaten. Die Funktion QGraphicsItem::setPos() setzt die Position des Objekts auf die angegebene Position im Koordinatensystem des Elternteils. Bei Elementen ohne Elternteil wird die angegebene Position als Szenenkoordinaten interpretiert. QGraphicsItem bietet auch eine Funktion mapToParent(), um eine in Elementkoordinaten angegebene Position auf das Koordinatensystem des Elternteils zu übertragen. Wenn das Element kein Elternteil hat, wird die Position stattdessen auf das Koordinatensystem der Szene abgebildet.
Dann 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 Framework der Grafikansicht das begrenzende Rechteck verwendet, um festzustellen, ob das Element neu gezeichnet werden muss, so dass alle Malvorgänge innerhalb dieses Rechtecks erfolgen müssen.
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 Objekts zu zeichnen; die Funktion zeichnet das Objekt in lokalen Koordinaten.
Beachten Sie die Bemalung der Ohren: Wenn ein Mauselement mit anderen Mauselementen kollidiert, werden seine Ohren rot gefüllt; andernfalls werden sie dunkelgelb gefüllt. Wir verwenden die Funktion QGraphicsScene::collidingItems(), um zu prüfen, ob es kollidierende Mäuse gibt. Die eigentliche Erkennung von Kollisionen wird vom Graphics View Framework mit Hilfe von Form-Formenteilung durchgeführt. Alles, was wir tun müssen, ist sicherzustellen, 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 Überschneidung beliebiger Formen um eine Größenordnung zunimmt, wenn die Formen komplex sind, kann dieser Vorgang sehr zeitaufwendig sein. Ein alternativer Ansatz besteht darin, die Funktion collidesWithItem() neu zu implementieren, um einen eigenen Algorithmus für die Kollision von Objekten und Formen zu erstellen.
Damit ist die Implementierung der Klasse Mouse
abgeschlossen; sie ist nun einsatzbereit. Werfen wir einen Blick auf die Funktion main()
, um zu sehen, wie man eine Szene für die Mäuse und eine Ansicht für die Anzeige des Inhalts der Szene implementiert.
Die Main()-Funktion
Die Funktion main()
stellt das Hauptanwendungsfenster bereit und erstellt die Elemente, ihre Szene und eine entsprechende Ansicht.
int main(int argc, char **argv) { QApplication app(argc, argv);
Zunächst wird ein Anwendungsobjekt erstellt und die Szene angelegt:
QGraphicsScene scene; scene.setSceneRect(-300, -300, 600, 600);
Die Klasse QGraphicsScene dient als Container für QGraphicsItems. Sie stellt auch Funktionen zur Verfügung, mit denen Sie die Position von Elementen effizient bestimmen können und die festlegen, welche Elemente innerhalb eines beliebigen Bereichs der Szene sichtbar sind.
Bei der Erstellung einer Szene ist es empfehlenswert, das Rechteck der Szene festzulegen; das Rechteck, das die Ausdehnung der Szene definiert. Es wird hauptsächlich von QGraphicsView verwendet, um den standardmäßigen scrollbaren Bereich der Ansicht zu bestimmen, und von QGraphicsScene, um die Indizierung der Elemente zu verwalten. Wenn es nicht explizit festgelegt wird, ist das Standardrechteck der Szene das größte Begrenzungsrechteck aller Elemente in der Szene seit der Erstellung der Szene. Das bedeutet, dass sich das Rechteck vergrößert, wenn Elemente in der Szene hinzugefügt oder verschoben werden, aber niemals verkleinert wird.
scene.setItemIndexMethod(QGraphicsScene::NoIndex);
Die Element-Indexfunktion wird verwendet, um das Auffinden von Elementen zu beschleunigen. NoIndex impliziert, dass das Auffinden von Elementen von linearer Komplexität ist, da alle Elemente in der Szene durchsucht werden. Das Hinzufügen, Verschieben und Entfernen von Objekten erfolgt jedoch in konstanter Zeit. Dieser Ansatz ist ideal für dynamische Szenen, in denen viele Elemente kontinuierlich hinzugefügt, verschoben oder entfernt werden. Die Alternative ist BspTreeIndex, die eine binäre Suche verwendet, um Algorithmen zur Lokalisierung von Objekten zu erreichen, die eine logarithmische Komplexität aufweisen.
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); }
Dann fügen wir die Mäuse in die Szene ein.
QGraphicsView view(&scene); view.setRenderHint(QPainter::Antialiasing); view.setBackgroundBrush(QPixmap(":/images/cheese.jpg"));
Um die Szene betrachten zu können, müssen wir auch ein QGraphicsView Widget erstellen. Die Klasse QGraphicsView visualisiert den Inhalt einer Szene in einem scrollbaren Ansichtsfenster. Wir stellen auch sicher, dass der Inhalt mit Antialiasing gerendert wird, und wir erstellen den Käse-Hintergrund, indem wir den Hintergrundpinsel der Ansicht einstellen.
Das Bild, das für den Hintergrund verwendet wird, wird als Binärdatei in der ausführbaren Datei der Anwendung unter Verwendung des Ressourcensystems von Qt gespeichert. Der QPixmap Konstruktor akzeptiert sowohl Dateinamen, die sich auf tatsächliche Dateien auf der Festplatte beziehen, als auch Dateinamen, die sich auf die eingebetteten Ressourcen der Anwendung beziehen.
view.setCacheMode(QGraphicsView::CacheBackground); view.setViewportUpdateMode(QGraphicsView::BoundingRectViewportUpdate); view.setDragMode(QGraphicsView::ScrollHandDrag);
Dann legen wir den Cache-Modus fest; QGraphicsView kann vorgerenderte Inhalte in einer Pixmap zwischenspeichern, die dann auf das Ansichtsfenster gezeichnet wird. Der Zweck einer solchen Zwischenspeicherung ist es, die gesamte Renderingzeit für Bereiche zu beschleunigen, die nur langsam gerendert werden können, z. B. 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 Setzen der Eigenschaft dragMode wird festgelegt, was geschehen soll, wenn der Benutzer auf den Hintergrund der Szene klickt und die Maus zieht. Mit dem Flag ScrollHandDrag verwandelt sich der Cursor in eine zeigende Hand, und durch Ziehen der Maus werden die Bildlaufleisten verschoben.
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(); }
Zum Schluss setzen wir den Titel und die Größe des Anwendungsfensters, bevor wir mit der Funktion QApplication::exec() in die Hauptereignisschleife eintreten.
Schließlich erstellen wir ein QTimer und verbinden sein timeout()-Signal mit dem advance()-Slot der Szene. Jedes Mal, wenn der Timer ausgelöst wird, wird die Szene um ein Bild weitergeschaltet.
Dann 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. Wenn man die Animation mit einer einzigen Timer-Verbindung ausführt, um die Szene voranzutreiben, wird sichergestellt, dass alle Mäuse an einem Punkt bewegt werden und, was noch wichtiger ist, dass nur eine Aktualisierung an den Bildschirm gesendet wird, nachdem sich alle Mäuse bewegt haben.
© 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.