Qtプログラミング必見!fitInView()でQGraphicsViewの表示を最適化

2025-05-27

QGraphicsView::fitInView() とは

QGraphicsView::fitInView() は、Qt の Graphics View Framework において、ビューポート(表示領域)内に指定されたアイテムまたはシーンの一部を最適なサイズで表示するための非常に便利な関数です。

QGraphicsView は、QGraphicsScene の内容を表示するためのウィジェットです。シーンには複数の QGraphicsItem(図形、画像、テキストなど)が配置されます。fitInView() を使うと、ビューポートのサイズに合わせてシーンの内容を自動的に拡大・縮小し、スクロールバーなしで全体を表示したり、指定した領域をビューポートにぴったり収まるように表示したりすることができます。

関数のオーバーロードと引数

fitInView() にはいくつかのオーバーロード(引数の異なる同名の関数)があります。

    • 特定の QGraphicsItem をビューポートに収めます。
    • item: ビューに収めたい QGraphicsItem へのポインタ。
    • aspectRatioMode: アスペクト比の扱い方を指定します。
      • Qt::IgnoreAspectRatio (デフォルト): アイテムのアスペクト比を無視して、ビューポート全体に引き伸ばして表示します。
      • Qt::KeepAspectRatio: アイテムのアスペクト比を維持しつつ、ビューポート内に最大限に拡大して表示します。余白(レターボックスまたはピラーボックス)ができる場合があります。
      • Qt::KeepAspectRatioByExpanding: アイテムのアスペクト比を維持しつつ、ビューポートを完全に埋めるように拡大します。アイテムの一部がビューポートからはみ出す場合があります。
  1. void QGraphicsView::fitInView(const QRectF &rect, Qt::AspectRatioMode aspectRatioMode = Qt::IgnoreAspectRatio)

    • シーン上の特定の矩形領域をビューポートに収めます。
    • rect: ビューに収めたいシーン座標での矩形領域。
    • aspectRatioMode: 上記と同様です。
  2. void QGraphicsView::fitInView() (引数なし)

    • 通常、この形式で呼び出すことは稀です。内部的には scene()->sceneRect() を引数に取った fitInView(scene()->sceneRect()) と同等に機能します。つまり、シーン全体の境界矩形をビューポートに収めようとします。

主な用途と動作

  • ズームのリセット: ユーザーがズーム操作を行った後に、元の全体表示に戻したい場合にも使用できます。
  • ウィンドウのリサイズ: QGraphicsView を含むウィンドウがリサイズされた際に、シーンの内容がビューポートの新しいサイズに合わせて自動的に調整されるように、resizeEvent() ハンドラ内で fitInView() を呼び出すのが一般的です。これにより、常に適切な表示を保つことができます。
  • 初期表示: アプリケーションが起動した際に、シーンの内容全体をビューに表示したい場合に便利です。

注意点

  • パフォーマンス: 大量のアイテムがあるシーンで頻繁に fitInView() を呼び出すと、再計算と再描画が発生するため、パフォーマンスに影響を与える可能性があります。
  • アイテムの boundingRect(): fitInView() は、アイテムの boundingRect() を利用してそのサイズと位置を計算します。もしアイテムの見た目のサイズと boundingRect() が一致しない場合(例えば、カスタムペイントで boundingRect() よりも大きく描画されている場合)、意図した通りにフィットしない可能性があります。
  • マージン: fitInView() は、指定されたアイテムや領域をビューポートに収める際に、デフォルトでわずかなマージン(余白)を追加することがあります。これにより、アイテムがビューポートの端にぴったりくっついて表示されるのを防ぎ、見た目を改善します。このマージンをなくしたい場合は、fitInView() を独自に再実装するか、ビューの変換行列を直接操作する必要があります。
#include <QApplication>
#include <QGraphicsScene>
#include <QGraphicsView>
#include <QGraphicsRectItem>
#include <QResizeEvent>

class MyGraphicsView : public QGraphicsView
{
public:
    MyGraphicsView(QGraphicsScene *scene, QWidget *parent = nullptr)
        : QGraphicsView(scene, parent)
    {
        // レンダリング品質の向上
        setRenderHint(QPainter::Antialiasing);
        setRenderHint(QPainter::SmoothPixmapTransform);
    }

protected:
    void resizeEvent(QResizeEvent *event) override
    {
        // ビューがリサイズされたときに、シーン全体をビューにフィットさせる
        fitInView(scene()->sceneRect(), Qt::KeepAspectRatio);
        QGraphicsView::resizeEvent(event); // 親クラスのresizeEventも呼び出す
    }
};

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

    QGraphicsScene scene;
    scene.setSceneRect(0, 0, 800, 600); // シーンの論理的なサイズを設定

    // シーンにアイテムを追加
    QGraphicsRectItem *rect1 = scene.addRect(0, 0, 200, 150, QPen(Qt::blue), QBrush(Qt::cyan));
    rect1->setPos(50, 50);

    QGraphicsRectItem *rect2 = scene.addRect(0, 0, 300, 200, QPen(Qt::red), QBrush(Qt::magenta));
    rect2->setPos(400, 300);

    MyGraphicsView view(&scene);
    view.setWindowTitle("QGraphicsView::fitInView() Example");
    view.resize(600, 400); // 初期ウィンドウサイズ

    // 最初に表示するときにシーン全体をフィットさせる
    view.fitInView(scene.sceneRect(), Qt::KeepAspectRatio);

    view.show();

    return a.exec();
}

この例では、MyGraphicsView クラスを継承し、resizeEvent をオーバーライドして、ウィンドウがリサイズされるたびにシーン全体 (scene()->sceneRect()) がアスペクト比を維持してビューにフィットするようにしています。



想定外のマージン(余白)が表示される

  • トラブルシューティング:
    • アスペクト比の確認: Qt::KeepAspectRatio を使用している場合は、余白が期待されるものかどうかを確認します。完全にビューポートを埋めたい場合は、Qt::IgnoreAspectRatio を試してみてください(ただし、アイテムが歪む可能性があります)。
    • sceneRect() の調整: QGraphicsScene::setSceneRect() を明示的に呼び出して、シーンの境界を実際のアイテムの結合された境界(scene->itemsBoundingRect())に設定します。
      // シーン上の全アイテムの境界に合わせる
      QRectF bounds = scene->itemsBoundingRect();
      scene->setSceneRect(bounds);
      view->fitInView(bounds, Qt::KeepAspectRatio);
      
      itemsBoundingRect() は、シーン内のすべてのアイテムの結合された境界を返します。これを使うことで、不要な空白領域を減らすことができます。
    • カスタムパディング: fitInView() のパディングを制御したい場合、fitInView() の代わりに手動で変換行列(QGraphicsView::setTransform())を計算し、適用することを検討します。これにより、より厳密な制御が可能です。
    • ビューポートのスタイル: QGraphicsView のフレームスタイル(setFrameStyle(QFrame::NoFrame)など)や、スタイルシート (setStyleSheet) を確認し、不要なボーダーがないか確認します。
  • 原因:
    • Qt::KeepAspectRatio の使用: Qt::KeepAspectRatio は、ビューポートのアスペクト比とシーン/アイテムのアスペクト比が異なる場合に、余白(レターボックスまたはピラーボックス)を生成します。これは仕様通りの動作です。
    • 内部的なパディング: fitInView()は、通常、指定された領域がビューポートの端にぴったりくっつかないように、内部的にわずかなパディング(数ピクセル)を追加します。これにより、見た目が改善されます。
    • シーンの sceneRect() が大きすぎる: fitInView(scene()->sceneRect()) を使用している場合、シーンの sceneRect() が実際に表示したいアイテムの境界よりもはるかに大きいと、その広い領域全体をフィットさせようとするため、余白が大きく見えます。
    • ビューポートの枠線: QGraphicsView 自体のフレームやボーダー、あるいは親ウィジェットのレイアウトやマージンが原因で、見た目の余白が生じている場合があります。
  • 問題: fitInView()を呼び出した後、アイテムやシーンの周囲に不必要な余白が表示される。

fitInView() を呼び出しても何も変化しない、または一部しか表示されない

  • トラブルシューティング:
    • アイテムの存在と可視性: シーンにアイテムが追加されており、それらが可視状態であるかを確認します。
    • boundingRect() のデバッグ: 各アイテムの boundingRect() をデバッグ出力などで確認し、意図したサイズと位置になっているか検証します。
    • 呼び出しタイミングの調整: QGraphicsView が完全に表示され、サイズが確定した後に fitInView() が呼び出されるようにします。最も一般的なのは、QGraphicsView を継承し、resizeEvent() をオーバーライドしてその中で fitInView() を呼び出す方法です。
      void MyGraphicsView::resizeEvent(QResizeEvent *event)
      {
          QGraphicsView::resizeEvent(event); // 必ず親クラスのメソッドを呼び出す
          fitInView(scene()->sceneRect(), Qt::KeepAspectRatio);
      }
      
      また、アプリケーション起動時の初期表示では、QMainWindow::showEvent()QWidget::showEvent() の中で一度だけ呼び出すことも有効です。
    • setSceneRect() の明示的な設定: シーンのコンテンツが動的に変化する場合や、特定の固定領域を表示したい場合は、QGraphicsScene::setSceneRect() を明示的に設定することを検討します。
  • 原因:
    • シーンにアイテムがない: fitInView() はシーン内のアイテムの境界に基づいてビューを調整します。シーンにアイテムが一つも追加されていない場合、またはアイテムが不可視になっている場合、計算する境界がないため正しく動作しません。
    • アイテムの boundingRect() が不正: 各 QGraphicsItemboundingRect() が正しく実装されていない場合、fitInView() は誤った領域に基づいて計算を行います。特にカスタムアイテムを作成している場合、boundingRect() が実際に描画される内容を正確にカバーしていることを確認してください。
    • fitInView() の呼び出しタイミング:
      • ビューのサイズがまだ決定されていない(例えば、ウィジェットがまだ表示されていない、またはレイアウト計算が完了していない)段階で fitInView() を呼び出すと、ビューポートのサイズが0になるため、正しくフィットしません。
      • showEvent()resizeEvent() の中で呼び出すのが適切です。
    • シーンの sceneRect() が設定されていない、または不正: シーンにアイテムがない場合や、sceneRect() を明示的に設定していない場合、デフォルトの sceneRect() が使用され、期待通りのフィットが得られないことがあります。
  • 問題: fitInView() を呼び出しても、ビューの内容が期待通りにフィットしないか、全く変化しない。

アイテムのサイズがビューのリサイズに追従しない

  • トラブルシューティング:
    • fitInView() は、ビューのズームレベルを調整する機能であるということを理解します。
    • もし、アイテムの論理的なサイズ自体をビューポートのサイズに合わせて変更したいのであれば、resizeEvent() の中で fitInView() を呼び出すだけでなく、シーン内のアイテムのサイズを手動で再計算して設定する必要があります。これは通常、より複雑なロジックを必要とします。
    • 例えば、ビューの現在の変換行列(transform())を取得し、それを使ってビュー座標をシーン座標に変換し、アイテムのサイズを決定する、といったアプローチが考えられます。
  • 原因:
    • fitInView() はビューの**変換行列(ズームレベル)**を調整することで、シーンの内容がビューポートにフィットするようにします。個々の QGraphicsItem 自体の論理的なサイズ(boundingRect() が返すサイズ)は変更しません。
    • ユーザーが期待しているのは、アイテムの論理的なサイズがビューポートのサイズに合わせて動的に変化することである場合、fitInView() だけでは不十分です。
  • 問題: ウィンドウをリサイズしても、シーン内のアイテムの見た目のサイズが変わらない。

fitInView() がパフォーマンスに影響を与える

  • トラブルシューティング:
    • 呼び出し頻度の最適化: 必要最小限の回数だけ fitInView() を呼び出すようにします。例えば、resizeEvent() の中や、明示的な「全体表示」ボタンがクリックされた時などです。
    • viewportUpdateMode の調整: QGraphicsView::setViewportUpdateMode() を設定することで、ビューポートの更新方法を最適化できる場合があります。デフォルトは QGraphicsView::MinimalViewportUpdate ですが、シーンの複雑さによっては QGraphicsView::BoundingRectViewportUpdateQGraphicsView::FullViewportUpdate を試すことで、レンダリングの問題が解決することもあります(ただし、パフォーマンスが低下する場合もあります)。
    • レンダリングヒント: setRenderHint(QPainter::Antialiasing) などのレンダリングヒントは、描画品質を向上させますが、パフォーマンスに影響を与えることがあります。必要に応じて無効にすることを検討します。
    • キャッシュモード: QGraphicsView::setCacheMode(QGraphicsView::CacheBackground) などで背景のキャッシュを有効にすると、特定のシナリオでパフォーマンスが改善されることがあります。
  • 原因:
    • fitInView() は、ビューポートの再計算とシーンの再描画を引き起こします。シーンに非常に多くの複雑なアイテムがある場合、この処理には時間がかかります。
    • 特に、頻繁に発生するイベント(例: マウスの移動中など)のハンドラ内で無条件に fitInView() を呼び出すと、描画が間に合わなくなることがあります。
  • 問題: fitInView() を頻繁に呼び出すと、アプリケーションが重くなる、またはフリーズする。

fitInView() は「シーン(またはアイテム)をビューポートに収める」という明確な目的を持っています。期待通りの動作が得られない場合は、通常、以下の点を再確認することで解決に導けます。

  1. fitInView() に渡している矩形(QRectF)またはアイテムの境界(boundingRect())が正しいか。
  2. fitInView() を呼び出す時点での QGraphicsView のサイズが正しいか。
  3. Qt::AspectRatioMode の選択が意図したものと一致しているか。


シーン全体をビューにフィットさせる(基本的な使用法)

最も一般的な使い方は、シーン全体のコンテンツをビューポートに合わせて表示することです。これは、ウィンドウのリサイズイベントなどでよく使用されます。

mainwindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QGraphicsView>
#include <QGraphicsScene>
#include <QGraphicsRectItem> // 四角形アイテム用
#include <QResizeEvent>     // リサイズイベント用

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

protected:
    // ウィンドウのリサイズイベントをオーバーライド
    void resizeEvent(QResizeEvent *event) override;

private:
    Ui::MainWindow *ui;
    QGraphicsScene *scene;
    QGraphicsView *view;
};
#endif // MAINWINDOW_H

mainwindow.cpp

#include "mainwindow.h"
#include "./ui_mainwindow.h"

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    scene = new QGraphicsScene(this);
    // シーンの論理的な境界を設定(オプション。アイテムが自動で広げることもできる)
    scene->setSceneRect(0, 0, 1000, 800); // 例: 幅1000, 高さ800のシーン

    // シーンにいくつかのアイテムを追加
    QGraphicsRectItem *rect1 = scene->addRect(100, 100, 200, 150, QPen(Qt::blue), QBrush(Qt::cyan));
    rect1->setFlags(QGraphicsItem::ItemIsMovable); // 動かせるようにする

    QGraphicsEllipseItem *ellipse1 = scene->addEllipse(400, 200, 250, 100, QPen(Qt::red), QBrush(Qt::yellow));
    ellipse1->setFlags(QGraphicsItem::ItemIsMovable);

    // ビューを作成し、シーンを設定
    view = new QGraphicsView(scene, this);
    setCentralWidget(view); // QMainWindow の中央ウィジェットとしてビューを設定

    // レンダリング品質の向上
    view->setRenderHint(QPainter::Antialiasing);
    view->setRenderHint(QPainter::SmoothPixmapTransform);

    // 初期表示時にシーン全体をビューにフィットさせる
    // ウィンドウが表示される前にビューのサイズが確定しないため、
    // resizeEventなどで呼び出すか、QTimer::singleShotなどを使用するのが安全。
    // ここでは、resizeEventで処理するので初回は不要だが、明示的に呼び出す例として残す。
    view->fitInView(scene->sceneRect(), Qt::KeepAspectRatio);
}

MainWindow::~MainWindow()
{
    delete ui;
    // sceneとviewは親オブジェクト(this)によって削除されるため、明示的なdeleteは不要
    // もし親オブジェクトを持たない場合は delete scene; delete view; が必要
}

void MainWindow::resizeEvent(QResizeEvent *event)
{
    // ウィンドウがリサイズされたときに、シーン全体をビューにフィットさせる
    // Qt::KeepAspectRatio を使うと、アスペクト比を維持しつつ最大限に拡大する
    // 余白(レターボックス/ピラーボックス)ができる可能性がある
    view->fitInView(scene->sceneRect(), Qt::KeepAspectRatio);

    // 親クラスのresizeEventも呼び出す
    QMainWindow::resizeEvent(event);
}

説明:

  • 最も重要なのは、resizeEvent() をオーバーライドし、その中で view->fitInView(scene->sceneRect(), Qt::KeepAspectRatio); を呼び出している点です。これにより、ウィンドウサイズが変更されるたびに、シーン全体がビューポート内にアスペクト比を維持して表示されるようになります。
  • view->setRenderHint() でアンチエイリアシングなどのレンダリング品質を向上させます。
  • QGraphicsRectItemQGraphicsEllipseItem など、いくつかのアイテムをシーンに追加します。
  • scene->setSceneRect() でシーンの論理的な境界を設定します。
  • MainWindow クラスで QGraphicsSceneQGraphicsView を作成します。

特定のアイテムをビューにフィットさせる

シーン全体ではなく、特定のアイテムをビューの中心に拡大表示したい場合に使用します。

mainwindow.cpp (一部変更)

// ... (前述のコードの続き)

class MainWindow : public QMainWindow
{
    // ... (前述の定義と同じ)

private:
    Ui::MainWindow *ui;
    QGraphicsScene *scene;
    QGraphicsView *view;
    QGraphicsRectItem *targetRect; // フィットさせる対象のアイテム
};

// ... (コンストラクタ内で)
MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    scene = new QGraphicsScene(this);
    scene->setSceneRect(0, 0, 1000, 800);

    // シーンにいくつかのアイテムを追加
    QGraphicsRectItem *rect1 = scene->addRect(100, 100, 200, 150, QPen(Qt::blue), QBrush(Qt::cyan));
    rect1->setFlags(QGraphicsItem::ItemIsMovable);

    // このアイテムをfitInViewのターゲットにする
    targetRect = scene->addRect(500, 400, 300, 200, QPen(Qt::darkGreen), QBrush(Qt::lightGray));
    targetRect->setFlags(QGraphicsItem::ItemIsMovable);

    QGraphicsEllipseItem *ellipse1 = scene->addEllipse(400, 200, 250, 100, QPen(Qt::red), QBrush(Qt::yellow));
    ellipse1->setFlags(QGraphicsItem::ItemIsMovable);

    view = new QGraphicsView(scene, this);
    setCentralWidget(view);

    view->setRenderHint(QPainter::Antialiasing);
    view->setRenderHint(QPainter::SmoothPixmapTransform);

    // 初期表示では、特定のアイテムをフィットさせる
    view->fitInView(targetRect, Qt::KeepAspectRatio);

    // (オプション) ボタンを追加して、そのボタンが押されたら特定のアイテムをフィットさせる
    QPushButton *fitButton = new QPushButton("Fit Target Rect", this);
    ui->verticalLayout->addWidget(fitButton); // assuming you have a verticalLayout in your UI file

    connect(fitButton, &QPushButton::clicked, this, [this](){
        view->fitInView(targetRect, Qt::KeepAspectRatio);
    });
}

void MainWindow::resizeEvent(QResizeEvent *event)
{
    // リサイズ時も特定のアイテムをフィットさせる
    view->fitInView(targetRect, Qt::KeepAspectRatio);
    QMainWindow::resizeEvent(event);
}

説明:

  • ボタンを追加し、クリックイベントで fitInView(targetRect, Qt::KeepAspectRatio); を呼び出すことで、ユーザーが任意のタイミングで特定のアイテムにズームインできるようにしています。
  • コンストラクタの初期表示や resizeEvent の中で、view->fitInView(targetRect, Qt::KeepAspectRatio); とすることで、常にこの特定の四角形がビューポートに収まるようにズームとスクロールが調整されます。
  • targetRect という QGraphicsRectItem を作成し、これを fitInView のターゲットとして使用します。

特定の矩形領域をビューにフィットさせる

シーン上の特定の座標範囲をビューにフィットさせることも可能です。これは、例えば、ユーザーが選択した領域を拡大表示したい場合などに役立ちます。

mainwindow.cpp (一部変更)

// ... (前述のコードの続き)

class MainWindow : public QMainWindow
{
    // ... (前述の定義と同じ)

private:
    Ui::MainWindow *ui;
    QGraphicsScene *scene;
    QGraphicsView *view;
    QRectF interestingRect; // フィットさせる対象の矩形領域
};

// ... (コンストラクタ内で)
MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    scene = new QGraphicsScene(this);
    scene->setSceneRect(0, 0, 1000, 800);

    QGraphicsRectItem *rect1 = scene->addRect(100, 100, 200, 150, QPen(Qt::blue), QBrush(Qt::cyan));
    rect1->setFlags(QGraphicsItem::ItemIsMovable);

    // 関心のある特定の矩形領域を定義
    interestingRect = QRectF(300, 300, 400, 300); // シーン座標 (300,300) からの幅400、高さ300の領域
    scene->addRect(interestingRect, QPen(Qt::green, 3)); // 視覚化のため枠線を追加

    QGraphicsEllipseItem *ellipse1 = scene->addEllipse(400, 200, 250, 100, QPen(Qt::red), QBrush(Qt::yellow));
    ellipse1->setFlags(QGraphicsItem::ItemIsMovable);

    view = new QGraphicsView(scene, this);
    setCentralWidget(view);

    view->setRenderHint(QPainter::Antialiasing);
    view->setRenderHint(QPainter::SmoothPixmapTransform);

    // 初期表示では、特定の矩形領域をフィットさせる
    view->fitInView(interestingRect, Qt::KeepAspectRatioByExpanding); // 余白なしで埋める

    // (オプション) ボタンを追加して、そのボタンが押されたら特定の矩形領域をフィットさせる
    QPushButton *fitRectButton = new QPushButton("Fit Interesting Rect", this);
    ui->verticalLayout->addWidget(fitRectButton);

    connect(fitRectButton, &QPushButton::clicked, this, [this](){
        view->fitInView(interestingRect, Qt::KeepAspectRatioByExpanding);
    });
}

void MainWindow::resizeEvent(QResizeEvent *event)
{
    // リサイズ時も特定の矩形領域をフィットさせる
    view->fitInView(interestingRect, Qt::KeepAspectRatioByExpanding);
    QMainWindow::resizeEvent(event);
}

説明:

  • view->fitInView(interestingRect, Qt::KeepAspectRatioByExpanding); を使用することで、この矩形領域がビューポートを完全に埋めるようにズームされます。Qt::KeepAspectRatioByExpanding を使用すると、アスペクト比を維持しつつ、ビューポート全体を埋めるように拡大されるため、一部がはみ出す可能性がありますが、余白はできません。
  • scene->addRect(interestingRect, QPen(Qt::green, 3)); でその領域を視覚化するために緑色の枠線を追加しています。
  • QRectF interestingRect という変数で、シーン上の特定の矩形領域を定義します。


QGraphicsView::fitInView() の代替方法

fitInView() が内部で行っているのは、ビューの変換行列(QTransform)を計算し、それをビューに適用することです。つまり、シーンの座標系をビューポートのサイズに合わせて拡大・縮小・移動させることで、指定された領域を表示しています。

このメカニズムを理解すれば、fitInView() を使わずに手動でズームやパンを実装する方法が見えてきます。

QGraphicsView::setTransform() を直接操作する

QGraphicsView は、現在の表示状態を QTransform オブジェクトとして保持しています。この QTransform を直接操作することで、ズーム、パン、回転などを自由に制御できます。

概念

  1. 現在の変換行列の取得: QGraphicsView::transform() で現在の QTransform を取得します。
  2. 新しい変換の適用: QTransform::scale(), QTransform::translate(), QTransform::rotate() などの関数を使って、既存の行列に新しい変換を結合します。
  3. ビューへの設定: QGraphicsView::setTransform() で新しい変換行列をビューに適用します。

手動ズームの例(マウスホイールイベント)

// MyGraphicsView.h (QGraphicsViewを継承したカスタムクラス)
#ifndef MYGRAPHICSVIEW_H
#define MYGRAPHICSVIEW_H

#include <QGraphicsView>
#include <QWheelEvent>
#include <QMouseEvent>
#include <QTransform>

class MyGraphicsView : public QGraphicsView
{
    Q_OBJECT
public:
    explicit MyGraphicsView(QGraphicsScene *scene, QWidget *parent = nullptr);

protected:
    void wheelEvent(QWheelEvent *event) override;
    void mousePressEvent(QMouseEvent *event) override;
    void mouseMoveEvent(QMouseEvent *event) override;
    void mouseReleaseEvent(QMouseEvent *event) override;

private:
    qreal _scaleFactor; // 現在のスケールファクターを保持
    bool _panning;      // パン中かどうかのフラグ
    QPoint _lastPanPoint; // パン開始時のマウス位置
};

#endif // MYGRAPHICSVIEW_H

// MyGraphicsView.cpp
#include "MyGraphicsView.h"
#include <QDebug> // デバッグ出力用

MyGraphicsView::MyGraphicsView(QGraphicsScene *scene, QWidget *parent)
    : QGraphicsView(scene, parent), _scaleFactor(1.0), _panning(false)
{
    // マウスホイールのアンカーをマウスカーソル位置にする
    setTransformationAnchor(AnchorUnderMouse);
    // レンダリング品質の向上
    setRenderHint(QPainter::Antialiasing);
    setRenderHint(QPainter::SmoothPixmapTransform);
}

void MyGraphicsView::wheelEvent(QWheelEvent *event)
{
    qreal scaleChange = 1.15; // ズーム倍率

    if (event->angleDelta().y() > 0) {
        // ホイールを上に回したらズームイン
        scale(scaleChange, scaleChange);
        _scaleFactor *= scaleChange;
    } else {
        // ホイールを下に回したらズームアウト
        scale(1.0 / scaleChange, 1.0 / scaleChange);
        _scaleFactor /= scaleChange;
    }
    event->accept(); // イベントを処理済みとしてマーク
}

void MyGraphicsView::mousePressEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton) { // 左クリックでパンを開始
        _panning = true;
        _lastPanPoint = event->pos(); // マウスの現在位置を保存
        setCursor(Qt::ClosedHandCursor); // カーソルを手の形にする
        event->accept();
    } else {
        QGraphicsView::mousePressEvent(event); // デフォルトの処理も呼び出す
    }
}

void MyGraphicsView::mouseMoveEvent(QMouseEvent *event)
{
    if (_panning) {
        // ビュー座標での移動量
        QPointF delta = mapToScene(_lastPanPoint) - mapToScene(event->pos());
        centerOn(sceneRect().center() + delta); // ビューの中心を移動量分ずらす

        _lastPanPoint = event->pos(); // 次の移動に備えて現在のマウス位置を保存
        event->accept();
    } else {
        QGraphicsView::mouseMoveEvent(event); // デフォルトの処理も呼び出す
    }
}

void MyGraphicsView::mouseReleaseEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton) { // パンを終了
        _panning = false;
        setCursor(Qt::ArrowCursor); // カーソルを元に戻す
        event->accept();
    } else {
        QGraphicsView::mouseReleaseEvent(event); // デフォルトの処理も呼び出す
    }
}

説明

  • パン (mousePressEvent, mouseMoveEvent, mouseReleaseEvent):
    • マウスボタンが押されたときにパンを開始するフラグを立て、開始位置を記録します。
    • マウスが移動するたびに、前回の位置からの移動量を計算します。
    • mapToScene() を使ってビュー座標での移動量をシーン座標に変換します。これにより、ズームレベルに関わらず一貫したパン動作が得られます。
    • centerOn() を使って、ビューの中心を新しい位置に移動させます。
    • カーソルを「握り手」のアイコンに変更し、パン中であることを視覚的に示します。
  • ズーム (wheelEvent): QGraphicsView::scale(sx, sy) メソッドは、現在の変換行列にスケーリングを適用します。setTransformationAnchor(AnchorUnderMouse) を使うことで、マウスカーソルの位置を中心にズームが行われます。

QGraphicsView::setSceneRect() と QGraphicsView::centerOn() を組み合わせる

fitInView() が内部で計算しているのは、ビューポートに表示すべきシーンの矩形領域と、その領域がビューポートの中心に来るようにする変換です。これを手動で計算して適用することもできます。

概念

  1. 表示したいシーンの領域を決定: QRectF で目的の領域を定義します。
  2. ビューポートのアスペクト比とシーンのアスペクト比を比較: ズーム倍率を計算します。
  3. 変換行列を計算: スケールと平行移動の値を計算し、QTransform オブジェクトを作成します。
  4. ビューに適用: setTransform() または scale()centerOn() を使ってビューを調整します。

手動で「フィット」させる例(より複雑な計算)

// QGraphicsViewのサブクラス内で
void MyGraphicsView::manualFitInView(const QRectF &targetRect, Qt::AspectRatioMode mode)
{
    if (targetRect.isNull()) {
        // ターゲットが不正な場合は何もしないか、エラー処理を行う
        return;
    }

    // 1. ビューポートのサイズを取得
    QRectF viewportRect = this->viewport()->rect();
    if (viewportRect.isEmpty()) {
        // ビューポートがまだサイズを持っていない場合は何もしない
        return;
    }

    // 2. ズーム倍率の計算
    qreal xRatio = viewportRect.width() / targetRect.width();
    qreal yRatio = viewportRect.height() / targetRect.height();

    qreal scaleFactor = 1.0;
    switch (mode) {
    case Qt::IgnoreAspectRatio:
        // アスペクト比を無視してビューポートにぴったり合わせる
        // この場合、XとYで異なるスケールを適用する必要があるため、setTransformを直接使う
        resetTransform(); // 現在の変換をリセット
        scale(xRatio, yRatio);
        centerOn(targetRect.center());
        return; // このケースは特殊なのでここで終了
    case Qt::KeepAspectRatio:
        // アスペクト比を維持しつつ、最大限に拡大(余白ができる可能性あり)
        scaleFactor = qMin(xRatio, yRatio);
        break;
    case Qt::KeepAspectRatioByExpanding:
        // アスペクト比を維持しつつ、ビューポートを完全に埋める(一部がはみ出す可能性あり)
        scaleFactor = qMax(xRatio, yRatio);
        break;
    default:
        return; // 未知のモード
    }

    // 3. 現在の変換をリセットし、新しいスケールを適用
    resetTransform(); // 現在の変換をリセットして、基準を単位行列に戻す
    scale(scaleFactor, scaleFactor);

    // 4. 指定された領域の中心がビューポートの中心に来るように移動
    // setTransformationAnchor(AnchorViewCenter) がデフォルトであれば、
    // scale()後にcenterOn()を呼び出すのが最も自然。
    centerOn(targetRect.center());

    // fitInViewのパディングを再現したい場合、ここで微調整を行う
    // 例えば、少しだけズームアウトする (例: scale(0.95, 0.95))
    // または、targetRect を少し拡大した矩形として計算に使用する
}

// このメソッドをresizeEventなどで呼び出す
void MyGraphicsView::resizeEvent(QResizeEvent *event)
{
    QGraphicsView::resizeEvent(event);
    // シーン全体をフィットさせる例
    manualFitInView(scene()->sceneRect(), Qt::KeepAspectRatio);
}

説明

  • centerOn(point): 指定されたシーン座標の点がビューポートの中心に来るようにビューを移動します。
  • scale(sx, sy): 現在のビューのスケールファクターを変更します。
  • resetTransform(): ビューの現在の変換行列を単位行列にリセットします。これは、新しい変換を最初から適用する際に重要です。

QGraphicsView::ensureVisible() の使用

ensureVisible()fitInView() とは少し異なり、「指定されたアイテムや領域がビューポート内に見えるようにする」機能を提供します。必ずしもビュー全体にフィットさせるわけではなく、必要に応じてスクロールバーを調整したり、わずかにズームアウトしたりします。

用途

  • 新しいアイテムがシーンに追加された際に、そのアイテムがビューポート内に見えるようにしたい。
  • ユーザーが選択したアイテムが画面外に移動した場合、自動的にそのアイテムが表示されるようにしたい。
// 特定のアイテムが追加された後、それがビューポートに表示されるようにする
QGraphicsRectItem *newItem = scene->addRect(1000, 1000, 50, 50);
view->ensureVisible(newItem, 50, 50); // アイテムの周囲に50ピクセルのマージンを持って表示
  • xmarginymargin は、アイテムの周囲に確保したい余白のピクセル数を指定します。
  • ensureVisible() は、指定されたアイテムまたは矩形がビューポート内に表示されるように、必要に応じてビューをスクロールおよびズームします。
  • 既存の機能との連携: QGraphicsView は、setDragMode(QGraphicsView::ScrollHandDrag) のような組み込みのパン機能も持っています。簡単なパンだけであれば、これを活用するのも良いでしょう。
  • パフォーマンス: fitInView() は Qt の内部で最適化されています。手動で変換を適用する場合も、setTransform() は比較的効率的ですが、paintEvent 内で重い計算を頻繁に行わないように注意が必要です。
  • 複雑さ: 手動で変換行列を操作する場合、特にアスペクト比の計算やマウス座標の変換(ビュー座標 <=> シーン座標)など、数学的な計算が必要になるため、実装が複雑になりがちです。
  • 制御の粒度: fitInView() は非常に高レベルなAPIであり、簡単に「フィット」機能を実現できます。しかし、ズームの挙動(例: ズームの中心点、特定の軸のみのズーム)や、パンの速度、カスタムアニメーションなど、より細かい制御が必要な場合は、setTransform() を直接操作する方が適しています。