Qtプログラミング徹底解説: QGraphicsScene::itemsBoundingRect()の基本

2025-05-27

QGraphicsScene::itemsBoundingRect() とは何か

QGraphicsScene::itemsBoundingRect() は、QtのGraphics View Frameworkで使用されるQGraphicsSceneクラスのメンバー関数です。この関数は、**シーン内のすべてのアイテム(QGraphicsItemのインスタンス)を包含する最小の矩形領域(バウンディングレクト)**を返します。

この関数は、以下のような場面で特に役立ちます。



QGraphicsScene::itemsBoundingRect() は便利ですが、その挙動を正しく理解していないと、意図しない結果を招くことがあります。

想定外のQRectFが返される(サイズが0、NaNなど)

  • 考えられる原因とトラブルシューティング:
    • シーンにアイテムが追加されていない: 最も単純なケースです。scene->addItem(myGraphicsItem);のように、何らかのQGraphicsItemがシーンに追加されているか確認してください。
    • アイテムのboundingRect()が不正: 各QGraphicsItemは自身のバウンディングレクトをboundingRect()仮想関数で定義します。もしカスタムアイテムの場合、このboundingRect()QRectF(0,0,0,0)を返している、または不正な座標を含んでいる可能性があります。
      • トラブルシューティング: 各カスタムアイテムのboundingRect()の実装を確認し、アイテムが持つべき正しいサイズと位置を返しているか検証してください。特に、アイテムのデータがまだロードされていない、または計算が完了していない段階でboundingRect()が呼ばれると、一時的に不正な値を返すことがあります。
    • アイテムが変形(transform)している: アイテムにスケール、回転、平行移動などの変換が適用されている場合、itemsBoundingRect()はそれらの変換を考慮した上で計算します。アイテムが非常に大きくスケールされていたり、遠く離れた位置に移動していたりすると、予期せず広大な範囲を返すことがあります。
      • トラブルシューティング: アイテムに適用されている変換を確認し、意図しない変換がかかっていないか確認します。必要に応じてQGraphicsItem::setTransform()QGraphicsItem::setPos()などで調整します。
    • QGraphicsItem::ItemIgnoresTransformationsフラグ: 特定のQGraphicsItemQGraphicsItem::ItemIgnoresTransformationsフラグを設定している場合、そのアイテムは親やビューの変換を無視します。この場合、itemsBoundingRect()の計算に影響を与える可能性があります。アイテムが意図しない位置に描画されたり、バウンディングレクトがずれたりすることが報告されています。
      • トラブルシューティング: このフラグが本当に必要か再検討します。もし使用している場合は、アイテムのジオメトリがビューとシーンで別々に管理されるため、座標変換(deviceTransform()など)を正しく扱う必要があります。
  • エラーの状況: itemsBoundingRect()QRectF(0,0,0,0)(空の矩形)や、不正な値(NaNなど)を返す。

パフォーマンスの問題(大規模なシーンで遅い)

  • 考えられる原因とトラブルシューティング:
    • 計算コストが高い: itemsBoundingRect()は、シーン内のすべてのアイテムを走査してバウンディングレクトを計算するため、アイテム数に比例して計算コストが増大します。
    • トラブルシューティング:
      • 呼び出し頻度の削減: 必要な場合にのみitemsBoundingRect()を呼び出すようにします。例えば、アイテムが追加/削除された時や、ビューのサイズが変更された時など、シーンの表示範囲に影響を与える変更があった時だけ再計算するようにします。
      • setSceneRect()の利用: シーンの範囲が比較的固定されている場合、QGraphicsScene::setSceneRect()で明示的にシーンの矩形を設定することを検討します。これにより、itemsBoundingRect()の呼び出しをなくすか、削減できます。setSceneRect()を設定すると、itemsBoundingRect()は内部的なインデックス管理には使用されなくなります。
      • カスタムのバウンディングレクト管理: 特定の目的のために、表示されているアイテムのみのバウンディングレクトが必要な場合など、itemsBoundingRect()が提供する機能が過剰である場合は、必要なアイテムだけを走査して手動でバウンディングレクトを計算する独自の関数を実装することも可能です。
  • エラーの状況: アイテム数が非常に多いシーンでitemsBoundingRect()を頻繁に呼び出すと、アプリケーションがフリーズしたり、処理が遅くなったりする。

シーンやビューが予期せず移動する

  • 考えられる原因とトラブルシューティング:
    • シーンのアイテムの動的な追加/削除: itemsBoundingRect()は、その呼び出し時点での全アイテムの範囲を返します。もしアイテムが追加されたり、移動したりすると、バウンディングレクトも変化するため、fitInView()を呼び出すたびにビューの位置が調整されます。これが「移動している」ように見える原因となることがあります。
    • setSceneRect()との相互作用: setSceneRect()が明示的に設定されていない場合、QGraphicsSceneは内部的にitemsBoundingRect()を使用してシーンの有効な範囲を決定します。アイテムが追加されるたびにこの内部的な範囲が更新され、それがビューの挙動に影響を与えることがあります。
    • トラブルシューティング:
      • setSceneRect()を固定する: シーンの範囲を一定に保ちたい場合は、QGraphicsScene::setSceneRect()を使って明示的にシーンの矩形を設定します。これにより、アイテムの追加や移動によってitemsBoundingRect()が変化しても、ビューのスクロール範囲や初期表示位置が固定されます。ただし、設定したsceneRectの範囲外にアイテムが配置された場合、そのアイテムはビューに表示されなくなるので注意が必要です。
      • fitInView()の適切な使用: fitInView()は、ビューにシーン全体を収めるためのものです。ビューの移動を避けたい場合は、シーンの範囲を固定するか、ビューのスクロールバーの設定(QGraphicsView::setVerticalScrollBarPolicy()など)を調整することを検討します。
  • エラーの状況: itemsBoundingRect()に基づいてQGraphicsView::fitInView()を呼び出すと、シーン内のアイテムがビュー内でわずかにずれたり、ビューが予期せず移動したりする。

特定のアイテムをitemsBoundingRect()の計算から除外したい

  • 考えられる原因とトラブルシューティング:
    • itemsBoundingRect()はすべてのアイテムを対象とするため、特定のアイテムを除外する機能は組み込まれていません。
    • トラブルシューティング:
      • 手動での計算: QGraphicsScene::items()で全てのアイテムリストを取得し、そのリストを走査して、除外したいアイテム以外のQGraphicsItem::sceneBoundingRect()を結合して、独自のバウンディングレクトを計算します。
        QRectF myItemsBoundingRect(QGraphicsScene* scene) {
            QRectF rect;
            for (QGraphicsItem* item : scene->items()) {
                // 特定の条件でアイテムを除外する
                if (item->data(0) == "background_item") { // 例: カスタムプロパティで判断
                    continue;
                }
                rect = rect.united(item->sceneBoundingRect());
            }
            return rect;
        }
        
      • アイテムの可視性で制御: もし除外したいアイテムが普段は非表示であるか、一時的に非表示にできるのであれば、QGraphicsItem::setVisible(false)にしてからitemsBoundingRect()を呼び出すことで、そのアイテムを計算から除外できます。ただし、そのアイテムを表示したい場合は、再びsetVisible(true)にする必要があります。
  • エラーの状況: シーンにルーラーや背景画像など、常に画面に表示され、かつ他のアイテムの表示範囲に影響を与えたくないアイテムがある場合、itemsBoundingRect()がそれらのアイテムも含めて広大な範囲を返してしまう。


例1: シーン内のアイテムに合わせてビューの表示を調整する

これはitemsBoundingRect()の最も一般的な使用例です。シーンにアイテムを追加した後、ビューがそれら全てのアイテムを収めるように表示を調整します。

#include <QApplication>
#include <QGraphicsScene>
#include <QGraphicsView>
#include <QGraphicsRectItem>
#include <QGraphicsEllipseItem>
#include <QVBoxLayout>
#include <QPushButton>
#include <QWidget>
#include <QLabel>
#include <QDebug>

class MyGraphicsView : public QGraphicsView
{
    Q_OBJECT
public:
    MyGraphicsView(QGraphicsScene* scene, QWidget* parent = nullptr)
        : QGraphicsView(scene, parent)
    {
        // ビューの背景色を設定
        setBackgroundBrush(Qt::lightGray);
        setRenderHint(QPainter::Antialiasing); // アンチエイリアスを有効にする
    }

public slots:
    void fitSceneToItems()
    {
        // シーン内の全アイテムを包含する矩形を取得
        QRectF bounds = scene()->itemsBoundingRect();
        qDebug() << "Items Bounding Rect:" << bounds;

        // その矩形がビューに収まるように表示を調整
        // Qt::KeepAspectRatioWithData を使うと、アスペクト比を維持しつつ、
        // アイテムのデータ(例: 描画範囲)を考慮してフィットさせます。
        // 単純にアスペクト比を維持したいだけなら Qt::KeepAspectRatio を使います。
        fitInView(bounds, Qt::KeepAspectRatio);
    }
};

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    // QGraphicsSceneを作成
    QGraphicsScene scene;

    // シーンにいくつかのアイテムを追加
    QGraphicsRectItem* rect1 = new QGraphicsRectItem(0, 0, 100, 50);
    rect1->setPos(50, 50);
    rect1->setBrush(Qt::blue);
    scene.addItem(rect1);

    QGraphicsEllipseItem* ellipse1 = new QGraphicsEllipseItem(0, 0, 80, 80);
    ellipse1->setPos(200, 100);
    ellipse1->setBrush(Qt::red);
    scene.addItem(ellipse1);

    QGraphicsRectItem* rect2 = new QGraphicsRectItem(0, 0, 70, 120);
    rect2->setPos(-100, -80); // 負の座標にも配置
    rect2->setBrush(Qt::green);
    scene.addItem(rect2);

    // MyGraphicsViewを作成
    MyGraphicsView view(&scene);
    view.setWindowTitle("QGraphicsScene::itemsBoundingRect() Example");
    view.resize(600, 400);

    // レイアウトを作成し、ビューとボタンを配置
    QWidget window;
    QVBoxLayout* layout = new QVBoxLayout(&window);
    layout->addWidget(&view);

    QPushButton* fitButton = new QPushButton("シーン全体を表示にフィット");
    layout->addWidget(fitButton);

    QLabel* infoLabel = new QLabel(&window);
    infoLabel->setText("初期状態ではアイテムは一部しか表示されないかもしれません。");
    layout->addWidget(infoLabel);

    // ボタンがクリックされたら fitSceneToItems() スロットを呼び出す
    QObject::connect(fitButton, &QPushButton::clicked, &view, &MyGraphicsView::fitSceneToItems);

    window.show();

    // 初期表示で一度フィットさせる
    view.fitSceneToItems();

    return a.exec();
}

#include "main.moc" // mocファイルをインクルード

解説:

  1. QGraphicsSceneに複数のQGraphicsItem(矩形や楕円)を追加します。アイテムはシーン内の異なる位置に配置されます。
  2. MyGraphicsViewクラスを継承したカスタムビューを作成し、fitSceneToItems()スロットを定義します。
  3. fitSceneToItems()スロット内で、scene()->itemsBoundingRect()を呼び出して、シーン内の全アイテムを包含する最小の矩形を取得します。
  4. 取得した矩形をfitInView()に渡すことで、QGraphicsViewがその矩形全体を表示領域に収まるように自動的にズームとパンを行います。
  5. ボタンをクリックすると、この処理が実行され、シーン内のアイテムがビュー全体に表示されるようになります。初期表示でも一度呼び出しています。

itemsBoundingRect()は、シーンの論理的な境界であるsceneRect()を動的に設定する際にも使われます。sceneRectが設定されていない場合、QGraphicsSceneはデフォルトでitemsBoundingRect()を内部的に使用しますが、明示的に設定することで、シーンの動作をより細かく制御できます。

#include <QApplication>
#include <QGraphicsScene>
#include <QGraphicsView>
#include <QGraphicsRectItem>
#include <QPushButton>
#include <QVBoxLayout>
#include <QWidget>
#include <QLabel>
#include <QTimer>
#include <QDebug>

class SceneRectUpdater : public QWidget
{
    Q_OBJECT
public:
    SceneRectUpdater(QWidget* parent = nullptr) : QWidget(parent)
    {
        scene = new QGraphicsScene(this);
        view = new QGraphicsView(scene, this);
        view->setRenderHint(QPainter::Antialiasing);
        view->setBackgroundBrush(Qt::white);

        infoLabel = new QLabel("アイテムを追加して、シーンの範囲が自動調整されるのを見てください。", this);

        QPushButton* addButton = new QPushButton("ランダムな四角形を追加", this);
        QPushButton* updateRectButton = new QPushButton("itemsBoundingRect()でsceneRectを更新", this);
        QPushButton* clearButton = new QPushButton("全てのアイテムを削除", this);

        QVBoxLayout* layout = new QVBoxLayout(this);
        layout->addWidget(infoLabel);
        layout->addWidget(view);
        layout->addWidget(addButton);
        layout->addWidget(updateRectButton);
        layout->addWidget(clearButton);

        connect(addButton, &QPushButton::clicked, this, &SceneRectUpdater::addRandomRect);
        connect(updateRectButton, &QPushButton::clicked, this, &SceneRectUpdater::updateSceneRect);
        connect(clearButton, &QPushButton::clicked, this, &SceneRectUpdater::clearScene);

        // 初期状態ではsceneRectを設定しない(アイテムに基づいて自動調整される)
        // scene->setSceneRect(0, 0, 400, 300); // 任意で初期サイズを設定することも可能
    }

private slots:
    void addRandomRect()
    {
        qreal x = QRandomGenerator::global()->bounded(-200.0, 200.0);
        qreal y = QRandomGenerator::global()->bounded(-150.0, 150.0);
        qreal w = QRandomGenerator::global()->bounded(20.0, 100.0);
        qreal h = QRandomGenerator::global()->bounded(20.0, 100.0);

        QGraphicsRectItem* rect = new QGraphicsRectItem(0, 0, w, h);
        rect->setPos(x, y);
        rect->setBrush(QColor(QRandomGenerator::global()->bounded(256),
                              QRandomGenerator::global()->bounded(256),
                              QRandomGenerator::global()->bounded(256)));
        scene->addItem(rect);

        // アイテム追加後に itemsBoundingRect() を取得し、情報を表示
        QRectF currentBounds = scene->itemsBoundingRect();
        qDebug() << "Current Items Bounding Rect:" << currentBounds;
        infoLabel->setText(QString("アイテムを追加しました。Items Bounding Rect: x=%1, y=%2, w=%3, h=%4")
                               .arg(currentBounds.x()).arg(currentBounds.y())
                               .arg(currentBounds.width()).arg(currentBounds.height()));

        // アイテム追加後、ビューをフィットさせる(自動調整)
        view->fitInView(currentBounds, Qt::KeepAspectRatio);
    }

    void updateSceneRect()
    {
        // itemsBoundingRect() の結果を sceneRect に明示的に設定
        QRectF bounds = scene->itemsBoundingRect();
        scene->setSceneRect(bounds);
        qDebug() << "Scene Rect updated to:" << bounds;
        infoLabel->setText(QString("sceneRectをItems Bounding Rectに更新しました。"));

        // 更新された sceneRect に合わせてビューをフィットさせる
        view->fitInView(bounds, Qt::KeepAspectRatio);
    }

    void clearScene()
    {
        scene->clear(); // シーンから全てのアイテムを削除
        qDebug() << "Scene cleared.";
        infoLabel->setText("全てのアイテムを削除しました。");

        // アイテムがなくなると itemsBoundingRect() は空のQRectF()を返す可能性がある
        // その場合、fitInViewは0,0,0,0の矩形を扱うことになるため、ビューが小さくなる
        // ここでsceneRectを明示的にリセットすることも検討
        // scene->setSceneRect(0, 0, 1, 1); // 例えば小さな矩形に設定
        updateSceneRect(); // アイテムがない場合はQRectF(0,0,0,0)を返す
    }

private:
    QGraphicsScene* scene;
    QGraphicsView* view;
    QLabel* infoLabel;
};

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    SceneRectUpdater updater;
    updater.setWindowTitle("QGraphicsScene::itemsBoundingRect() & setSceneRect() Example");
    updater.resize(700, 500);
    updater.show();

    return a.exec();
}

#include "main.moc" // mocファイルをインクルード

解説:

  1. この例では、SceneRectUpdaterというQWidgetを継承したクラスを作成し、QGraphicsSceneQGraphicsViewを含めます。
  2. addRandomRect()スロットでは、ランダムな位置とサイズの矩形をシーンに追加します。アイテムが追加されるたびにitemsBoundingRect()を呼び出し、その結果をデバッグ出力とQLabelに表示します。そして、ビューをその新しいバウンディングレクトにフィットさせます。
  3. updateSceneRect()スロットでは、明示的にitemsBoundingRect()の結果をscene->setSceneRect()に設定します。これにより、シーンの論理的な境界が、実際に存在するアイテムの範囲に合わせられます。
  4. clearScene()スロットでは、全てのアイテムを削除した後、再びupdateSceneRect()を呼び出します。アイテムがない場合、itemsBoundingRect()はデフォルトでQRectF(0,0,0,0)を返します。この挙動は、fitInView()の動作に影響を与えます。もしアイテムが全くない状態でビューを初期化したいが、0,0,0,0にしたくない場合は、scene->setSceneRect(初期の望ましい範囲)のように明示的に設定する必要があります。


QGraphicsScene::sceneRect を明示的に設定する

  • コード例:
    #include <QGraphicsScene>
    #include <QGraphicsView>
    #include <QGraphicsRectItem>
    #include <QApplication>
    #include <QVBoxLayout>
    #include <QPushButton>
    #include <QWidget>
    #include <QDebug>
    
    int main(int argc, char *argv[])
    {
        QApplication a(argc, argv);
    
        QGraphicsScene scene;
        // シーンの範囲を明示的に設定
        scene.setSceneRect(-200, -150, 400, 300); // x, y, width, height
    
        QGraphicsRectItem* rect1 = new QGraphicsRectItem(0, 0, 100, 50);
        rect1->setPos(-180, -100);
        rect1->setBrush(Qt::blue);
        scene.addItem(rect1);
    
        QGraphicsEllipseItem* ellipse1 = new QGraphicsEllipseItem(0, 0, 80, 80);
        ellipse1->setPos(150, 80);
        ellipse1->setBrush(Qt::red);
        scene.addItem(ellipse1);
    
        // sceneRectの範囲外にアイテムを配置してみる
        QGraphicsRectItem* outOfBoundsRect = new QGraphicsRectItem(0, 0, 50, 50);
        outOfBoundsRect->setPos(300, 0); // sceneRectの右外
        outOfBoundsRect->setBrush(Qt::green);
        scene.addItem(outOfBoundsRect);
        qDebug() << "Out of bounds item added at (300, 0)";
    
        QGraphicsView view(&scene);
        view.setWindowTitle("Explicit sceneRect Example");
        view.resize(600, 400);
        view.show();
    
        // 明示的に設定されたsceneRectにビューをフィットさせる
        view.fitInView(scene.sceneRect(), Qt::KeepAspectRatio);
    
        // itemsBoundingRect() を呼び出しても、sceneRect に影響しないことを確認
        qDebug() << "itemsBoundingRect():" << scene.itemsBoundingRect(); // outOfBoundsRectも含まれる
        qDebug() << "sceneRect():" << scene.sceneRect(); // 明示的に設定した値
    
        return a.exec();
    }
    
  • デメリット:
    • 手動管理: アイテムが設定されたsceneRectの範囲外に移動した場合、それらのアイテムはビューに表示されなくなる可能性があります。開発者がシーンの範囲を常に最新の状態に保つ責任を負います。
    • フィットの複雑化: fitInView()を使用する場合、ビューのスクロール範囲はsceneRectに限定されるため、アイテム全体をフィットさせるには、itemsBoundingRect()を手動で計算してfitInView()に渡す必要があります。
  • メリット:
    • パフォーマンス向上: 大規模なシーンでitemsBoundingRect()の呼び出しが不要になるため、パフォーマンスが向上します。
    • 制御のしやすさ: シーンの論理的な境界を完全に制御できます。特に、常に同じ範囲を維持したい場合や、無限に広がるシーンを避けたい場合に有効です。
    • 予測可能性: アイテムの追加や削除によってシーンの境界が予期せず変化するのを防ぎます。

表示されているアイテムのみのバウンディングレクトを手動で計算する

  • コード例:
    #include <QApplication>
    #include <QGraphicsScene>
    #include <QGraphicsView>
    #include <QGraphicsRectItem>
    #include <QGraphicsEllipseItem>
    #include <QVBoxLayout>
    #include <QPushButton>
    #include <QWidget>
    #include <QLabel>
    #include <QDebug>
    
    // シーン内の表示されているアイテムのみのバウンディングレクトを計算する関数
    QRectF calculateVisibleItemsBoundingRect(QGraphicsScene* scene) {
        QRectF bounds;
        bool firstItem = true;
        for (QGraphicsItem* item : scene->items()) {
            if (item->isVisible()) { // 表示されているアイテムのみを考慮
                if (firstItem) {
                    bounds = item->sceneBoundingRect();
                    firstItem = false;
                } else {
                    bounds = bounds.united(item->sceneBoundingRect());
                }
            }
        }
        return bounds;
    }
    
    int main(int argc, char *argv[])
    {
        QApplication a(argc, argv);
    
        QGraphicsScene scene;
    
        QGraphicsRectItem* visibleRect = new QGraphicsRectItem(0, 0, 100, 50);
        visibleRect->setPos(50, 50);
        visibleRect->setBrush(Qt::blue);
        scene.addItem(visibleRect);
    
        QGraphicsEllipseItem* hiddenEllipse = new QGraphicsEllipseItem(0, 0, 80, 80);
        hiddenEllipse->setPos(200, 200);
        hiddenEllipse->setBrush(Qt::red);
        hiddenEllipse->setVisible(false); // このアイテムは非表示
        scene.addItem(hiddenEllipse);
    
        QGraphicsView view(&scene);
        view.setWindowTitle("Custom Visible Items Bounding Rect Example");
        view.resize(600, 400);
    
        // 表示されているアイテムのみのバウンディングレクトを計算
        QRectF visibleBounds = calculateVisibleItemsBoundingRect(&scene);
        qDebug() << "Calculated Visible Items Bounding Rect:" << visibleBounds;
        qDebug() << "QGraphicsScene::itemsBoundingRect():" << scene.itemsBoundingRect();
    
        view.fitInView(visibleBounds, Qt::KeepAspectRatio);
        view.show();
    
        return a.exec();
    }
    
  • デメリット:
    • パフォーマンス: アイテム数が非常に多い場合、手動で全てのアイテムを走査するのはitemsBoundingRect()と同様に時間がかかる可能性があります。
    • コードの複雑化: 汎用的なitemsBoundingRect()に比べて、カスタムロジックを記述する必要があります。
  • メリット:
    • 柔軟性: 特定の条件に合致するアイテムのみの範囲を計算できます。
    • 精度の向上: 不要なアイテム(背景、非表示のアイテムなど)が計算に含まれるのを防ぎ、より正確な「表示されるべき」範囲を取得できます。

シーンのアイテムインデックスアルゴリズムを調整する

  • 注意: この方法は、itemsBoundingRect()の直接的な代替というよりは、itemsBoundingRect()が遅いと感じる根本原因(インデックスのオーバーヘッド)に対処するためのものです。通常は、itemsBoundingRect()が提供する利点を放棄するほど大きなパフォーマンス問題がない限り、推奨されません。
  • デメリット:
    • アイテム検索関数のパフォーマンス低下: items()itemAt()などのアイテム検索系の関数のパフォーマンスが大幅に低下します。
    • itemsBoundingRect()自体の効率低下: インデックスがないため、itemsBoundingRect()の計算も、全てのアイテムを単純に走査する形になるため、より非効率になる可能性があります。
  • メリット:
    • 動的なシーンでの潜在的なパフォーマンス改善: アイテムの移動が非常に多い場合に、インデックス更新のオーバーヘッドをなくすことで、全体的な応答性が向上する可能性があります。

シーンを仮想化する(高度なケース)

  • デメリット:
    • 実装の複雑性: シーンのスクロールやズームに合わせてアイテムを動的に管理するロジックを自分で実装する必要があります。
    • 一般的な用途ではない: ほとんどのアプリケーションではここまでの対応は不要です。
  • メリット:
    • 極めて大規模なデータセットの処理: 数十万、数百万のアイテムを扱う場合に有効です。
    • メモリ使用量の削減: 必要なアイテムのみがメモリに存在するため、メモリ効率が向上します。