QGraphicsView::ViewportAnchorのよくある落とし穴と解決策

2025-05-27

具体的には、以下の3つの値があります。

  • QGraphicsView::AnchorUnderMouse (2):

    • マウスカーソルの位置を基準点として拡大縮小またはリサイズを行います。
    • これは、CADソフトウェアなどでよく見られる挙動で、マウスカーソルが指しているシーン上の位置が、拡大縮小後もその位置に留まるように調整されます。
    • ユーザーは、マウスカーソルを特定のオブジェクトの上に置いてズームインすることで、そのオブジェクトに焦点を合わせることができます。
  • QGraphicsView::AnchorViewCenter (1):

    • ビューポートの中央を基準点として拡大縮小またはリサイズを行います。
    • つまり、ビューポートの中心に表示されているシーン上の位置が、拡大縮小後もビューポートの中心に留まるように調整されます。
    • これにより、ユーザーは現在のビューの中心を維持したまま、スムーズにズームイン・ズームアウトできます。
  • QGraphicsView::NoAnchor (0):

    • これはデフォルトの値です。
    • ビューポートを拡大縮小またはリサイズしても、シーン内の特定の位置が固定されることはありません。
    • ビューポートの中心も特に固定されず、自由に移動します。
    • 結果として、ズームイン・ズームアウトした際に、表示されている内容がビューポートの中心からずれてしまう可能性があります。

QGraphicsView::ViewportAnchor は、主に以下の2つのプロパティで設定されます。

  • QGraphicsView::setResizeAnchor(ViewportAnchor anchor):

    • ビューのリサイズの基準点を設定します。
    • QGraphicsView のウィジェット自体のサイズが変更されたときに、シーンの表示領域がどのように調整されるかを指定します。
  • QGraphicsView::setTransformationAnchor(ViewportAnchor anchor):

    • ビューの**変換(拡大縮小や回転など)**の基準点を設定します。
    • ズームイン・ズームアウトする際に、どの点を中心に拡大縮小するかを指定します。


意図しないズーム挙動(特に AnchorUnderMouse)

よくあるエラー

  • AnchorUnderMouse を設定したのに、マウスを動かしてもズームの中心が変わらない。
  • 初めてホイールズームを行ったときに、ビューが原点(0,0)にジャンプしてしまう。
  • マウスカーソルがある位置を中心にズームされるはずなのに、なぜかズームの中心がずれる。

原因とトラブルシューティング

  • ズーム係数の丸め誤差

    • 特に繰り返しズーム操作を行う場合、浮動小数点数の丸め誤差によって、わずかな位置ずれが生じることがあります。これは AnchorUnderMouse を使っているとより顕著に感じられることがあります。
    • 解決策
      厳密なピクセル合わせが必要な場合は、ズーム係数を整数ベースで管理したり、ズーム後に centerOn() を使って特定のポイントに強制的に合わせるなどの調整が必要になる場合があります。
  • シーン座標とビュー座標の混同

    • QGraphicsView はビュー座標(ウィジェットのピクセル座標)とシーン座標(グラフィックアイテムが配置される論理座標)の変換を行います。ズーム処理を自作する場合、これらの座標変換を正しく行う必要があります。
    • 例えば、マウス位置を基準にズームする場合、マウスのビュー座標をシーン座標に変換し、そのシーン座標をズームの中心として使用する必要があります。
    • QGraphicsView::mapToScene(QPointF viewPoint)QGraphicsView::mapFromScene(QPointF scenePoint) を適切に利用してください。
    • AnchorUnderMouse は、マウスカーソルの位置を常に追跡している必要があります。
    • QGraphicsView::setTransformationAnchor(QGraphicsView::AnchorUnderMouse) を設定すると、ビューポートの mouseTracking プロパティが自動的に true に設定されます。しかし、何らかの理由でこの設定が上書きされたり、ビューが非インタラクティブ (setInteractive(false)) に設定されている場合、マウス位置が正しく更新されず、ズームの中心が期待通りにならないことがあります。
    • 解決策
      ビューポートの mouseTrackingtrue に設定されていることを確認してください。
      myGraphicsView->viewport()->setMouseTracking(true);
      
    • また、特に wheelEvent など、特定のイベントハンドラ内で setTransformationAnchor を動的に変更している場合、最初のホイールイベント発生時にまだマウス位置が取得されていないことがあります。この場合、一時的に setInteractive(true) に設定し、偽のマウス移動イベントを送信してマウス位置を更新するワークアラウンドが考えられます。

NoAnchor 設定時の表示のずれ

よくあるエラー

  • setTransformationAnchor(QGraphicsView::NoAnchor) を使用している場合、ズームイン・ズームアウトすると、表示の中心がどんどんずれていってしまう。

原因とトラブルシューティング

  • 解決策
    通常、ユーザーの意図に合わせたズーム挙動を実現するには、AnchorViewCenter または AnchorUnderMouse を使用すべきです。NoAnchor は、ビューの変換を完全に手動で制御したい場合や、スクロールバーによってビューポートを移動させる場合などに限定的に使用します。
  • NoAnchor は文字通り「アンカーなし」なので、変換時にビューポートの相対位置を維持しません。これが意図しない挙動であれば、期待通りの動作ではありません。

リサイズ時の表示のずれ

よくあるエラー

  • ウィンドウや QGraphicsView のサイズを変更すると、シーンの内容が期待した位置に留まらない。

原因とトラブルシューティング

  • 解決策
    • リサイズ時にビューポートの中央を維持したい場合は、setResizeAnchor(QGraphicsView::AnchorViewCenter) を設定します。
    • マウスの位置を基準にリサイズしたい場合は、setResizeAnchor(QGraphicsView::AnchorUnderMouse) を設定します。
    • 特に設定しない場合 (NoAnchor) は、ビューポートの左上隅が固定されるため、ビューポートの中心が移動します。
  • setResizeAnchor() の設定が適切でない可能性があります。

カスタム変換との競合

よくあるエラー

  • 独自の QTransform を使用してビューの変換を行っているが、ViewportAnchor の設定が機能しない、または意図しない結果になる。

原因とトラブルシューティング

  • 解決策
    • できる限り QGraphicsView が提供する scale()translate() などの便利な関数を使用し、setTransformationAnchor() を活用することを推奨します。
    • どうしてもカスタムな QTransform を直接設定する必要がある場合は、setTransformationAnchor(QGraphicsView::NoAnchor) を設定し、変換後のビューポートのオフセットを自分で計算して centerOn() などで調整する必要があります。この場合、mapToScene()mapFromScene() を使って、ビューとシーン間の座標変換を正確に理解しておくことが重要です。
  • setTransform()scale(), rotate(), translate() などの関数は、ViewportAnchor の設定を考慮して内部的にビューを調整します。しかし、複雑なカスタム変換ロジックを実装している場合、これらの内部的な調整と競合する可能性があります。

よくあるエラー

  • ズームやスクロールがスムーズでなく、カクカクする。

原因とトラブルシューティング

  • 解決策
    • QGraphicsView::setViewportUpdateMode() の設定
      MinimalViewportUpdate(デフォルト)や BoundingRectViewportUpdate など、描画の更新範囲を最適化するモードを試します。
    • レンダリングヒントの調整
      setRenderHints(QPainter::Antialiasing) など、アンチエイリアシングは見た目を良くしますが、パフォーマンスコストが高くなります。必要に応じて調整します。
    • キャッシュの使用
      setCacheMode(QGraphicsView::CacheBackground | QGraphicsView::CacheItems) を使用して、描画に時間がかかる背景やアイテムのキャッシュを有効にします。
    • アイテムの最適化
      複雑な QGraphicsItemboundingRect()shape() が効率的か確認します。
    • OpenGLの利用
      setViewport(new QOpenGLWidget()) を使用して、OpenGLレンダリングに切り替えることで、ハードウェアアクセラレーションを活用し、パフォーマンスを向上させることができます。
  • ViewportAnchor 自体が直接パフォーマンスのボトルネックになることは稀ですが、ズームやリサイズによって大量の描画が発生する場合に、間接的に影響を与えることがあります。


main.cpp

#include <QApplication>
#include <QGraphicsScene>
#include <QGraphicsView>
#include <QGraphicsRectItem>
#include <QWheelEvent>
#include <QMouseEvent>
#include <QDebug> // デバッグ出力用

// カスタムQGraphicsViewクラス
class CustomGraphicsView : public QGraphicsView
{
public:
    CustomGraphicsView(QGraphicsScene* scene, QWidget* parent = nullptr)
        : QGraphicsView(scene, parent)
    {
        // 以下のいずれかのアンカーを設定することで、ズーム挙動が変わります
        // デフォルトは NoAnchor です

        // 1. ビューポートの中心を基準にズーム
        // setTransformationAnchor(QGraphicsView::AnchorViewCenter);

        // 2. マウスカーソルの位置を基準にズーム(最も直感的)
        setTransformationAnchor(QGraphicsView::AnchorUnderMouse);
        // AnchorUnderMouse を使う場合、マウスのトラッキングを有効にする必要があります
        // setTransformationAnchor() が自動的に有効にするはずですが、明示的に設定することもできます
        viewport()->setMouseTracking(true);

        // 3. アンカーなし(デフォルト)
        // setTransformationAnchor(QGraphicsView::NoAnchor);

        // リサイズ時のアンカーも設定できます
        // setResizeAnchor(QGraphicsView::AnchorViewCenter);

        // アンチエイリアシングを有効にして、描画を滑らかにする
        setRenderHint(QPainter::Antialiasing, true);
        setRenderHint(QPainter::SmoothPixmapTransform, true);

        // ドラッグモードを設定して、手動でスクロールできるようにする
        setDragMode(QGraphicsView::ScrollHandDrag);
    }

protected:
    // マウスホイールイベントをオーバーライドしてズーム機能を追加
    void wheelEvent(QWheelEvent* event) override
    {
        qreal scaleFactor = 1.15; // ズームの倍率

        if (event->angleDelta().y() > 0) {
            // ホイールを上にスクロール(ズームイン)
            scale(scaleFactor, scaleFactor);
        } else {
            // ホイールを下にスクロール(ズームアウト)
            scale(1.0 / scaleFactor, 1.0 / scaleFactor);
        }

        // QGraphicsView::wheelEvent() を呼び出すことで、デフォルトのスクロール動作も維持できます
        // もし完全にカスタムなズーム動作にしたい場合は、コメントアウトしても構いません
        // QGraphicsView::wheelEvent(event);
    }
};

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

    // シーンの作成
    QGraphicsScene scene;
    scene.setSceneRect(-200, -200, 400, 400); // シーンの範囲を設定

    // シーンにアイテムを追加
    QGraphicsRectItem *rect1 = scene.addRect(-100, -100, 50, 50, QPen(Qt::blue), QBrush(Qt::cyan));
    rect1->setFlags(QGraphicsItem::ItemIsMovable); // 移動可能にする

    QGraphicsEllipseItem *ellipse1 = scene.addEllipse(50, 50, 80, 80, QPen(Qt::red), QBrush(Qt::magenta));
    ellipse1->setFlags(QGraphicsItem::ItemIsMovable); // 移動可能にする

    QGraphicsTextItem *text1 = scene.addText("Hello Qt Graphics!");
    text1->setPos(-50, 100);
    text1->setFlags(QGraphicsItem::ItemIsMovable); // 移動可能にする

    // カスタムビューの作成とシーンの設定
    CustomGraphicsView view(&scene);
    view.setWindowTitle("QGraphicsView ViewportAnchor Example");
    view.resize(800, 600); // ウィンドウサイズ

    // ビューを中央に配置
    view.centerOn(0, 0);

    view.show();

    return app.exec();
}

プロジェクトファイル (.pro)

Qt Creator を使用している場合は、新しい Qt Widgets Application プロジェクトを作成し、上記の main.cpp の内容で置き換えます。.pro ファイルには通常以下の行が含まれます。

QT += widgets
SOURCES += main.cpp

ViewportAnchor の挙動

上記のコードで CustomGraphicsView のコンストラクタ内の setTransformationAnchor() のコメントアウトを切り替えることで、それぞれの挙動を確認できます。

    • 挙動: マウスホイールでズームイン・ズームアウトしても、ビューポートの中心は固定されません。ズームすると、オブジェクトがビューポートの左上方向に移動するように見えます。これは直感的ではない場合があります。
    • 用途: ビューの変換を完全に手動で制御したい場合や、スクロールバーを主体としたビューの場合に限定的に使用されます。
  1. setTransformationAnchor(QGraphicsView::AnchorViewCenter);

    • 挙動: マウスホイールでズームイン・ズームアウトすると、常にビューポートの中心に表示されているシーン上の位置がズームの中心となります。ビューポートのどの位置にマウスがあっても、中心が拡大・縮小されます。
    • 用途: 全体像を把握しながらズームしたい場合や、特定の位置に限定せずに中心を維持したい場合に適しています。
  2. setTransformationAnchor(QGraphicsView::AnchorUnderMouse);

    • 挙動: マウスホイールでズームイン・ズームアウトすると、マウスカーソルがある位置のシーン座標がズームの中心となります。マウスカーソルを特定のアイテムの上に置いてズームすると、そのアイテムが拡大されます。
    • 用途: CADソフトウェアや画像編集ソフトウェアのように、特定のオブジェクトや領域に焦点を当てて作業する場合に非常に便利で、最も直感的で一般的なズーム挙動です。このオプションを使用する場合、viewport()->setMouseTracking(true); が重要になります(setTransformationAnchor が自動的に行うこともありますが、明示的に設定しても問題ありません)。

setResizeAnchor() は、QGraphicsView ウィジェット自体のサイズが変更されたときに、どのようにビューポートが調整されるかを制御します。

たとえば、CustomGraphicsView のコンストラクタに以下の行を追加してみます。

// ウィンドウをリサイズしたときに、ビューポートの中央を維持する
setResizeAnchor(QGraphicsView::AnchorViewCenter);

// ウィンドウをリサイズしたときに、マウスカーソルの位置を維持する (あまり一般的ではないが、試すことは可能)
// setResizeAnchor(QGraphicsView::AnchorUnderMouse);
  • setResizeAnchor(QGraphicsView::NoAnchor); (デフォルト)

    • 挙動: ウィンドウをリサイズすると、ビューポートの左上隅が固定されます。そのため、ウィンドウサイズを大きくすると右下方向に表示領域が広がり、小さくすると右下方向が削られます。
  • setResizeAnchor(QGraphicsView::AnchorViewCenter);

    • 挙動: ウィンドウをリサイズすると、シーンの中央が常にビューポートの中央に位置するように調整されます。これにより、ウィンドウサイズを変更しても、表示内容の中心がずれることなく、全体像を維持しやすくなります。


主に、QGraphicsView の変換関数や、マウスイベント処理における座標変換を直接操作することで実現します。

マウス位置に基づくカスタムズーム (AnchorUnderMouse の代替)

AnchorUnderMouse は非常に便利ですが、もしこの機能を自前で実装したい場合、または QGraphicsView のデフォルトのズーム動作が何らかの理由で不十分な場合に有効です。

基本的な考え方

  1. マウスホイールイベント (wheelEvent) でズームの方向と量を検出します。
  2. ズームが適用される前に、現在のマウスカーソル位置(ビューポート座標)をシーン座標に変換します。
  3. QGraphicsView::scale() を呼び出してズームを実行します。
  4. ズーム後、元のマウスカーソル位置のシーン座標が、新しいスケールでのマウスカーソル位置に変換されるように、ビューポートを移動(スクロール)します。

コード例

#include <QGraphicsView>
#include <QWheelEvent>
#include <QPointF> // QPointF を使用

class CustomZoomView : public QGraphicsView
{
public:
    CustomZoomView(QGraphicsScene* scene, QWidget* parent = nullptr)
        : QGraphicsView(scene, parent)
    {
        // ViewportAnchor は設定しない (または NoAnchor に設定)
        setTransformationAnchor(QGraphicsView::NoAnchor);
        setRenderHint(QPainter::Antialiasing);
        setDragMode(QGraphicsView::ScrollHandDrag); // ドラッグでのスクロールも有効にする
    }

protected:
    void wheelEvent(QWheelEvent* event) override
    {
        // ズームイン・ズームアウト前のマウス位置 (ビューポート座標)
        QPointF viewPoint = event->position(); // Qt6 では position()
        // Qt5 では event->pos() を使用

        // マウス位置をシーン座標に変換
        QPointF scenePointBefore = mapToScene(viewPoint.toPoint()); // toPoint() は Qt6 の QPointF から QPoint への変換

        qreal scaleFactor = 1.15;
        if (event->angleDelta().y() > 0) {
            // ズームイン
            scale(scaleFactor, scaleFactor);
        } else {
            // ズームアウト
            scale(1.0 / scaleFactor, 1.0 / scaleFactor);
        }

        // ズーム後のマウス位置に対応するシーン座標
        QPointF scenePointAfter = mapToScene(viewPoint.toPoint());

        // ズーム前とズーム後のシーン座標の差分を計算
        QPointF delta = scenePointAfter - scenePointBefore;

        // 差分をビューポートの移動量に変換してスクロール
        // centerOn はビューポートの中心に指定したシーン座標を移動させる
        // delta はマウスカーソルを元のシーン座標に戻すためのオフセットとして機能
        centerOn(scenePointBefore - delta);

        // デフォルトのホイールイベント処理は行わない (スクロールハンドラーを使っているため)
        // event->accept(); // イベントを処理済みとしてマーク
    }
};

利点

  • 特定の条件でズームの中心を変えるなど、より複雑なカスタムロジックを組み込むことが可能です。
  • AnchorUnderMouse と同等の挙動を、より低レベルな制御で実現できます。

考慮点

  • 座標変換の理解が不可欠です。
  • ViewportAnchor を使うよりも多くのコードが必要になります。

ビューポートの中心を維持するカスタムズーム (AnchorViewCenter の代替)

AnchorViewCenter も同様に、手動で実装することが可能です。

基本的な考え方

  1. ビューポートの中心のシーン座標を事前に取得します。
  2. scale() を呼び出してズームを実行します。
  3. ズーム後に、最初に取得したシーン座標が再びビューポートの中心に来るように centerOn() を使用して移動します。

コード例

#include <QGraphicsView>
#include <QWheelEvent>
#include <QPointF>

class CustomCenterZoomView : public QGraphicsView
{
public:
    CustomCenterZoomView(QGraphicsScene* scene, QWidget* parent = nullptr)
        : QGraphicsView(scene, parent)
    {
        setTransformationAnchor(QGraphicsView::NoAnchor); // AnchorViewCenter を設定しない
        setRenderHint(QPainter::Antialiasing);
        setDragMode(QGraphicsView::ScrollHandDrag);
    }

protected:
    void wheelEvent(QWheelEvent* event) override
    {
        // 現在のビューポートの中心のシーン座標を取得
        QPointF centerScenePoint = mapToScene(viewport()->rect().center());

        qreal scaleFactor = 1.15;
        if (event->angleDelta().y() > 0) {
            scale(scaleFactor, scaleFactor);
        } else {
            scale(1.0 / scaleFactor, 1.0 / scaleFactor);
        }

        // ズーム後、ビューポートの中心が元のシーン座標になるように移動
        centerOn(centerScenePoint);

        // event->accept();
    }
};

利点

  • 特定の条件で中心を維持しない、あるいは異なる中心点を使用するなど、柔軟な制御が可能です。
  • AnchorViewCenter と同じ挙動を、より明示的なコードで実現できます。

リサイズ時のカスタム動作 (setResizeAnchor の代替)

setResizeAnchor の代替としては、ビューの resizeEvent をオーバーライドし、ウィジェットのサイズ変更時に手動でビューポートの変換を調整する方法があります。

基本的な考え方

  1. resizeEvent をオーバーライドします。
  2. 現在のビューポートの状態(特に中心点や表示範囲)を記録します。
  3. 親クラスの resizeEvent を呼び出して、ビューポートのサイズ変更を処理させます。
  4. 記録しておいた状態に基づいて、新しいビューポートサイズに合わせて centerOn()fitInView() などを使用してビューポートを調整します。

コード例

#include <QGraphicsView>
#include <QResizeEvent>
#include <QPointF>

class CustomResizeView : public QGraphicsView
{
public:
    CustomResizeView(QGraphicsScene* scene, QWidget* parent = nullptr)
        : QGraphicsView(scene, parent)
    {
        setResizeAnchor(QGraphicsView::NoAnchor); // AnchorViewCenter を設定しない
        setRenderHint(QPainter::Antialiasing);
        setDragMode(QGraphicsView::ScrollHandDrag);
    }

protected:
    void resizeEvent(QResizeEvent* event) override
    {
        // リサイズ前のビューポートの中心のシーン座標を記録
        QPointF oldCenterScenePoint = mapToScene(viewport()->rect().center());

        // 基底クラスの resizeEvent を呼び出す (ビューポートのサイズが更新される)
        QGraphicsView::resizeEvent(event);

        // 新しいサイズになったビューポートで、元の中心が維持されるように移動
        centerOn(oldCenterScenePoint);
    }
};

利点

  • リサイズ時に特定のシーン範囲を fitInView するなど、より高度な制御が可能です。
  • setResizeAnchor では不可能な、より複雑なリサイズ動作(例:アスペクト比を維持しながら特定のアイテムを常に表示するなど)を実装できます。

考慮点

  • 手動での座標計算と調整が必要になります。

QGraphicsView::ViewportAnchor は、Qt が提供する高レベルな抽象化であり、多くの一般的なズームおよびリサイズ要件を簡単に満たします。しかし、特定のカスタム動作が必要な場合や、デバッグ目的で内部動作を理解したい場合は、上記のような代替方法を検討することが役立ちます。