衝突するマウスの例

グラフィックスビュー上でアイテムをアニメーションさせる方法を示します。

Colliding Miceの例では、Graphics Viewフレームワークを使用して、アニメートされたアイテムを実装し、アイテム間の衝突を検出する方法を示します。

Graphics Viewは、QGraphicsItem クラスから派生した多数のカスタムメイドの2Dグラフィカル・アイテムを管理し、操作するためのQGraphicsScene クラスと、ズームと回転をサポートしたアイテムを視覚化するためのQGraphicsView ウィジェットを提供します。

Mouse クラスはQGraphicsItem を拡張した個々のマウスを表し、main() 関数はメイン・アプリケーション・ウィンドウを提供します。

まず、Mouse クラスをレビューして、アイテムのアニメーションとアイテムの衝突を検出する方法を確認します。次に、main() 関数をレビューして、アイテムをシーンに配置する方法と、対応するビューを実装する方法を確認します。

マウスクラスの定義

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 の2つの純粋な仮想パブリック関数を実装する必要があります。boundingRect() は、アイテムによって描画される領域の推定値を返し、paint() は、実際の描画を実装します。さらに、shape() とadvance() を再実装します。shape() を再実装して、マウス・アイテムの正確な形状を返します。デフォルトの実装では、単にアイテムの外接矩形を返します。advance() を再実装してアニメーションを処理し、1回の更新ですべてが行われるようにします。

マウスクラスの定義

マウスアイテムを作成するとき、まず、クラス内で直接初期化されていないアイテムのプライベート変数がすべて適切に初期化されていることを確認します:

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 がシーンを1フレーム進めると決めたら、各アイテムで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() が 2 回呼び出されるからです。1 回は step ==0 で、アイテムが進もうとしていることを示し、もう 1 回は 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フレームワークがShape-Shape Intersectionを使って処理します。私たちがしなければならないことは、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 、プリレンダリングされたコンテンツをpixmapにキャッシュし、それをビューポートに描画することができます。このようなキャッシュの目的は、例えばテクスチャ、グラデーション、アルファブレンドされた背景など、レンダリングに時間がかかる部分の総レンダリング時間を短縮することです。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() スロットに接続します。タイマーが作動するたびに、シーンは1フレーム進みます。

そして、1000/33ミリ秒ごとにタイマーが作動するようにします。これでフレームレートは1秒間に30フレームとなり、ほとんどのアニメーションには十分な速さになります。シーンを進めるために1つのタイマー接続でアニメーションを行うことで、すべてのマウスが1つのポイントで動かされ、さらに重要なことは、すべてのマウスが動いた後に1つの更新だけがスクリーンに送られることだ。

プロジェクト例 @ code.qt.io

©2024 The Qt Company Ltd. 本書に含まれる文書の著作権は、それぞれの所有者に帰属します。 本書で提供されるドキュメントは、Free Software Foundation が発行したGNU Free Documentation License version 1.3に基づいてライセンスされています。 Qtおよびそれぞれのロゴは、フィンランドおよびその他の国におけるThe Qt Company Ltd.の 商標です。その他すべての商標は、それぞれの所有者に帰属します。