もう迷わない!Qt QGraphicsViewのスクロール同期とカスタム動作

2025-05-27

QGraphicsView::scrollContentsBy() は、QtのGraphics Viewフレームワークで使用される仮想関数(virtual function)です。これは QAbstractScrollArea クラスから継承されており、ビューポート(表示領域)の内容を指定された量だけスクロールする際に呼び出されます。

役割

  • 再実装(Override)の機会: 通常、この関数を直接呼び出すことはあまりありません。しかし、QGraphicsView を継承したカスタムクラスで、スクロールイベントが発生したときに特別な処理を行いたい場合に、この関数を再実装(オーバーライド)することができます。
  • 差分によるスクロール: 引数 dxdy は、水平方向と垂直方向のスクロール量を示します。dx はX軸方向への移動量、dy はY軸方向への移動量(ピクセル単位)です。正の値は右または下へのスクロールを意味し、負の値は左または上へのスクロールを意味します。
  • ビューポートのスクロール処理: QGraphicsViewQGraphicsScene の内容を視覚化するためのウィジェットです。ユーザーがスクロールバーを操作したり、マウスドラッグ(ScrollHandDragモードなど)でビューポートを動かしたりすると、この scrollContentsBy() 関数が内部的に呼び出されます。

再実装する際の注意点

scrollContentsBy() を再実装する場合、必ず基底クラスの QGraphicsView::scrollContentsBy(dx, dy) を呼び出す必要があります。これを行わないと、QGraphicsView 自身が持つスクロール処理(シーンの描画領域の更新やスクロールバーの移動など)が正しく行われず、表示が崩れたり、スクロールが機能しなくなったりします。

#include <QGraphicsView>
#include <QDebug> // デバッグ出力用

class MyGraphicsView : public QGraphicsView
{
    Q_OBJECT // シグナル/スロットを使用する場合に必要

public:
    MyGraphicsView(QWidget *parent = nullptr) : QGraphicsView(parent)
    {
        // コンストラクタ
    }

protected:
    // scrollContentsBy をオーバーライド
    void scrollContentsBy(int dx, int dy) override
    {
        // まず基底クラスの実装を呼び出す
        QGraphicsView::scrollContentsBy(dx, dy);

        // ここにカスタムのスクロール処理を追加
        qDebug() << "View Scrolled By: dx =" << dx << ", dy =" << dy;

        // 例えば、スクロールに合わせてビューポートに固定されたアイテムを動かすなど
        // 注意: QGraphicsItem::ItemIgnoresTransformations フラグを設定することで、
        // アイテムがビューの変換(スクロールやズーム)を無視するようにすることも可能です。
        // この場合、明示的にアイテムを動かす必要はありません。
    }
};

使用例と応用

  • カスタムのスクロール動作: デフォルトのスクロール動作に加えて、特定の条件下でスクロール量を調整したり、視覚的なエフェクトを追加したりする場合に利用できます。
  • ビューポートに固定されたアイテムの同期: シーン内の特定のアイテムを、ビューポートの特定の場所に常に表示させたい場合(例えば、ミニマップやコントロールパネルなど)、scrollContentsBy() 内でそのアイテムのシーン座標を計算し直し、setPos() で移動させることで実現できます。ただし、前述の QGraphicsItem::ItemIgnoresTransformations フラグも検討する価値があります。
  • スクロールイベントの監視: ビューがスクロールしたことをプログラムで検知したい場合に、scrollContentsBy() をオーバーライドして、カスタムのシグナルを発行することができます。


void QGraphicsView::scrollContentsBy() の一般的なエラーとトラブルシューティング

基底クラスの scrollContentsBy() を呼び出し忘れる

エラーの症状

  • パフォーマンスが著しく低下する。
  • シーンの描画がおかしくなる(描画残像、アイテムが正しく更新されないなど)。
  • スクロールバーが動いても、表示内容が変わらない。
  • ビューポートが全くスクロールしない。

原因
QGraphicsView::scrollContentsBy() をオーバーライドする際、基底クラスの同名関数を呼び出していないためです。基底クラスの scrollContentsBy() は、ビューポートの実際のスクロール処理(シーンのオフセット調整、表示範囲の更新、スクロールバーの同期など)を担当しています。これを呼び出さないと、これらの基本的な機能が動作しません。

トラブルシューティング
オーバーライドした scrollContentsBy() の中で、一番最初に必ず基底クラスの関数を呼び出すようにしてください。

void MyGraphicsView::scrollContentsBy(int dx, int dy)
{
    // これが非常に重要です!
    QGraphicsView::scrollContentsBy(dx, dy);

    // ここにカスタムの処理を追加
    // 例: qDebug() << "View Scrolled By: dx =" << dx << ", dy =" << dy;
}

スクロールによる描画のアーティファクト(描画の乱れ)

エラーの症状

  • スクロールがカクカクしたり、スムーズでない。
  • アイテムの一部が欠けたり、重複して表示されたりする。
  • スクロール時に以前の描画が残ってしまう。

原因

  • 最適化フラグの影響
    QGraphicsViewsetViewportUpdateMode()setCacheMode() の設定によっては、意図しない描画のアーティファクトが発生することがあります。
  • QGraphicsItem::boundingRect() や shape() の不正確さ
    QGraphicsItemboundingRect() がアイテムの実際の描画範囲を正確に返していない場合、Qtは更新が必要な領域を正しく判断できず、描画残像が発生することがあります。
  • QGraphicsScene の invalidate() の不適切または不足
    シーンのキャッシュが無効化されていないため、古い描画データが使われてしまう。特に、スクロールによって背景や一部のアイテムの表示状態が変わる場合に発生しやすいです。
  • update() や viewport()->update() の不足
    スクロール後にビューポートの再描画が適切にトリガーされていない。

トラブルシューティング

  • setViewportUpdateMode(QGraphicsView::FullViewportUpdate) を試すと、アーティファクトが解消される場合がありますが、パフォーマンスが低下する可能性があります。問題が解決しない場合は、段階的に QGraphicsView::MinimalViewportUpdateQGraphicsView::SmartViewportUpdate を試して、パフォーマンスと品質のバランスを見つけてください。
  • QGraphicsItem をカスタムしている場合、boundingRect() がそのアイテムの描画範囲を正確にカバーしていることを確認してください。shape() も衝突検出などに影響するため、必要に応じて正確に実装されているか確認します。
  • シーンのキャッシュを無効化する必要があるか検討してください。特にカスタムの背景描画を行っている場合や、スクロールに合わせてアイテムの表示が複雑に変化する場合に有効です。
    QGraphicsView::scrollContentsBy(dx, dy);
    scene()->invalidate(scene()->sceneRect(), QGraphicsScene::BackgroundLayer); // 必要に応じて
    // または scene()->invalidate(rect_to_update);
    
  • scrollContentsBy() の基底クラス呼び出し後に、ビューポートの更新を明示的に要求してみてください。
    QGraphicsView::scrollContentsBy(dx, dy);
    viewport()->update(); // または update();
    

スクロールと連動してカスタムアイテムを動かす際の不整合

エラーの症状

  • 複数の QGraphicsView を同期させようとしたときに、一方のビューがスクロールしてももう一方が正確に追従しない。
  • ビューポートに固定したいアイテムが、スクロール時にずれてしまう。

原因

  • タイミングの問題
    scrollContentsBy() 内でアイテムの位置を更新するタイミングが、Qtの内部的なスクロール処理と同期していない。
  • 浮動小数点誤差
    多くのスクロール処理が連続して行われると、浮動小数点数の計算誤差が蓄積され、わずかなずれが生じることがあります。
  • 座標変換の誤り
    viewport 座標と scene 座標、または他のカスタム座標系との間で変換ミスがある。

トラブルシューティング

  • デバッグ出力と視覚的な検証
    qDebug() を使って dxdy の値、およびアイテムの更新後の座標を頻繁に出力し、想定通りの値になっているか確認してください。また、スクロール動作を動画に記録したり、スローモーションで実行したりして、アーティファクトの発生箇所やタイミングを特定するのも有効です。
  • スクロールバーの valueChanged シグナルとの同期
    複数のビューを同期させる場合、scrollContentsBy() をオーバーライドする代わりに、各ビューのスクロールバーの valueChanged シグナルを接続し、そのシグナルハンドラ内で他のビューのスクロールバーの値を setValue() で設定することで同期させることができます。これにより、再帰呼び出しや描画の問題を回避しやすくなります。
    // コンストラクタなどで接続
    connect(view1->horizontalScrollBar(), &QScrollBar::valueChanged,
            [view2](int value){ view2->horizontalScrollBar()->setValue(value); });
    // 同様に verticalScrollBar も
    
  • mapToScene() を使用した正確な座標計算
    カスタムでアイテムの位置を調整する場合、QGraphicsView::mapToScene() を使って、ビューポートの特定の点(例えば中央)がシーン上のどこに対応するかを正確に計算し、それに基づいてアイテムの位置を更新します。
    void MyGraphicsView::scrollContentsBy(int dx, int dy)
    {
        QGraphicsView::scrollContentsBy(dx, dy);
    
        // 例: ビューポートの左上隅を常に基準にしたい場合
        QPointF newTopLeftInScene = mapToScene(viewport()->rect().topLeft());
        myCustomItem->setPos(newTopLeftInScene); // あるいは、任意のオフセットを適用
    }
    
  • QGraphicsItem::ItemIgnoresTransformations フラグの利用
    ビューポートに常に固定したい(ズームやスクロールの影響を受けない)アイテムがある場合、そのアイテムに QGraphicsItem::ItemIgnoresTransformations フラグを設定することを検討してください。これにより、scrollContentsBy() 内で明示的にアイテムを動かす必要がなくなります。
    myFixedItem->setFlag(QGraphicsItem::ItemIgnoresTransformations, true);
    
    ただし、このフラグを設定したアイテムは、ビューの拡大縮小(ズーム)も無視します。ズームには追従させたいがスクロールには追従させたくない、といった場合は、このフラグは使えません。

パフォーマンスの問題

エラーの症状

  • CPU使用率が高い。
  • スクロールが重い、フレームレートが低い。

原因

  • キャッシュの無効化のしすぎ
    QGraphicsItem::setCacheMode()QGraphicsView::setCacheMode() を適切に利用していない。
  • 複雑なアイテムの描画
    QGraphicsItempaint() 関数が重い処理を含んでいる、または大量のアイテムがある場合に、スクロールごとに多くの描画が発生する。
  • 過剰な再描画
    scrollContentsBy() 内で不必要に広範囲の update()invalidate() を呼び出している。
  • アイテム数の管理
    大量のアイテムがある場合は、表示されている領域のアイテムのみをシーンに追加・削除する、あるいはLOD(Level of Detail)を利用して、遠くのアイテムは簡略化して描画するといった最適化を検討してください。
  • paint() 関数の最適化
    QGraphicsItem::paint() 関数内で、重い計算やファイルI/Oを行わないようにします。描画はできるだけ軽量に保つべきです。
  • QGraphicsView::setViewportUpdateMode() の調整
    デフォルトの MinimalViewportUpdate が最も効率的ですが、アーティファクトが発生する場合は SmartViewportUpdateFullViewportUpdate を試してみてください。しかし、パフォーマンスが問題になる場合は、MinimalViewportUpdate に戻すか、repaint() を必要最小限の領域に限定することを検討してください。
  • QGraphicsItem::setCacheMode() の活用
    静的な(あまり変化しない)アイテムや複雑な描画を持つアイテムに対しては、QGraphicsItem::DeviceCoordinateCache または QGraphicsItem::ItemCoordinateCache を設定することで、再描画のコストを削減できます。これにより、スクロール時にアイテム全体を再描画する代わりに、キャッシュされた画像が使用されます。
  • 最小限の再現コードを作成する
    複雑なアプリケーション全体ではなく、問題が発生する最小限の QGraphicsView のセットアップだけを含むシンプルなアプリケーションを作成し、問題を切り分けます。
  • QGraphicsView::viewport()->setContentsMargins()
    もしビューポートの表示領域が期待通りでない場合は、マージン設定が影響している可能性もあります。
  • Qt Creatorのデバッガを使用する
    ブレークポイントを設定して、スクロールイベントがどのように伝播し、scrollContentsBy() がいつ、どのスレッドで呼び出されているかを追跡します。
  • qDebug() でログを出力する
    scrollContentsBy(int dx, int dy) がいつ、どのくらいの量で呼び出されているかを把握するために、dxdy の値を出力します。


scrollContentsBy() をオーバーライドしてスクロール情報をデバッグ出力する

最も基本的な例として、スクロールが発生したときにその情報をコンソールに出力する例です。これは、スクロールがいつ、どれだけ行われたかを把握するのに役立ちます。

mygraphicsview.h

#ifndef MYGRAPHICSVIEW_H
#define MYGRAPHICSVIEW_H

#include <QGraphicsView>
#include <QDebug> // デバッグ出力用

class MyGraphicsView : public QGraphicsView
{
    Q_OBJECT // シグナル/スロットを使用する場合に必要

public:
    explicit MyGraphicsView(QWidget *parent = nullptr);
    MyGraphicsView(QGraphicsScene *scene, QWidget *parent = nullptr);

protected:
    // QGraphicsView::scrollContentsBy() をオーバーライド
    void scrollContentsBy(int dx, int dy) override;
};

#endif // MYGRAPHICSVIEW_H

mygraphicsview.cpp

#include "mygraphicsview.h"

MyGraphicsView::MyGraphicsView(QWidget *parent)
    : QGraphicsView(parent)
{
}

MyGraphicsView::MyGraphicsView(QGraphicsScene *scene, QWidget *parent)
    : QGraphicsView(scene, parent)
{
}

void MyGraphicsView::scrollContentsBy(int dx, int dy)
{
    // !!! 重要 !!! 必ず基底クラスの関数を呼び出す
    QGraphicsView::scrollContentsBy(dx, dy);

    // ここにカスタムのスクロール処理を追加
    qDebug() << "ビューがスクロールしました: dx =" << dx << ", dy =" << dy;
}

main.cpp (使用例)

#include <QApplication>
#include <QMainWindow>
#include <QGraphicsScene>
#include <QGraphicsRectItem>
#include <QVBoxLayout>
#include <QWidget>
#include "mygraphicsview.h"

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

    QMainWindow window;
    window.setWindowTitle("QGraphicsView::scrollContentsBy() Example 1");

    QGraphicsScene *scene = new QGraphicsScene(-500, -500, 1000, 1000); // 広いシーン
    scene->addRect(-200, -200, 400, 400, QPen(Qt::blue), QBrush(Qt::lightGray));
    scene->addEllipse(50, 50, 100, 100, QPen(Qt::red), QBrush(Qt::cyan));
    scene->addText("Hello, Qt Graphics View!", QFont("Arial", 50));

    MyGraphicsView *view = new MyGraphicsView(scene);
    view->setDragMode(QGraphicsView::ScrollHandDrag); // マウスドラッグでスクロール可能にする

    QWidget *centralWidget = new QWidget;
    QVBoxLayout *layout = new QVBoxLayout(centralWidget);
    layout->addWidget(view);
    window.setCentralWidget(centralWidget);

    window.resize(600, 400);
    window.show();

    return a.exec();
}

このコードを実行し、ビューをスクロールすると、コンソールにスクロール量がデバッグ出力されます。

ビューポートに固定されたオーバーレイアイテムの同期

scrollContentsBy() を使用する一般的なシナリオの一つは、シーンのスクロールとは独立して、常にビューポートの特定の場所に表示され続けるアイテムを管理することです。

mygraphicsview.h (上記に加えて)

#ifndef MYGRAPHICSVIEW_H
#define MYGRAPHICSVIEW_H

#include <QGraphicsView>
#include <QGraphicsTextItem> // オーバーレイ表示用
#include <QDebug>

class MyGraphicsView : public QGraphicsView
{
    Q_OBJECT

public:
    explicit MyGraphicsView(QWidget *parent = nullptr);
    MyGraphicsView(QGraphicsScene *scene, QWidget *parent = nullptr);

protected:
    void scrollContentsBy(int dx, int dy) override;
    void resizeEvent(QResizeEvent *event) override; // リサイズイベントも処理

private:
    QGraphicsTextItem *overlayTextItem; // ビューポートに固定するアイテム
    void updateOverlayPosition();
};

#endif // MYGRAPHICSVIEW_H

mygraphicsview.cpp (上記に加えて)

#include "mygraphicsview.h"
#include <QResizeEvent>

MyGraphicsView::MyGraphicsView(QWidget *parent)
    : QGraphicsView(parent),
      overlayTextItem(nullptr)
{
    // コンストラクタでオーバーレイアイテムを初期化(シーンが設定されてから)
}

MyGraphicsView::MyGraphicsView(QGraphicsScene *scene, QWidget *parent)
    : QGraphicsView(scene, parent),
      overlayTextItem(nullptr)
{
    // オーバーレイテキストアイテムを作成し、シーンに追加
    overlayTextItem = new QGraphicsTextItem("オーバーレイテキスト");
    overlayTextItem->setDefaultTextColor(Qt::white);
    overlayTextItem->setFont(QFont("Arial", 20, QFont::Bold));
    if (scene) {
        scene->addItem(overlayTextItem);
        // アイテムがビューの変換(スクロールやズーム)を無視するように設定
        // ただし、位置は手動で調整する必要がある
        overlayTextItem->setFlag(QGraphicsItem::ItemIgnoresTransformations, true);
    }
    updateOverlayPosition(); // 初期位置を設定
}

void MyGraphicsView::scrollContentsBy(int dx, int dy)
{
    QGraphicsView::scrollContentsBy(dx, dy);

    // スクロール時にオーバーレイアイテムの位置を更新
    updateOverlayPosition();
    qDebug() << "ビューがスクロールしました: dx =" << dx << ", dy =" << dy;
}

void MyGraphicsView::resizeEvent(QResizeEvent *event)
{
    QGraphicsView::resizeEvent(event);
    // ビューのリサイズ時にもオーバーレイアイテムの位置を更新
    updateOverlayPosition();
    qDebug() << "ビューがリサイズしました";
}

void MyGraphicsView::updateOverlayPosition()
{
    if (overlayTextItem && viewport()) {
        // ビューポートの右下隅から特定のオフセットに配置
        // mapToScene() を使ってビュー座標をシーン座標に変換する
        QPointF viewBottomRight = viewport()->rect().bottomRight();
        // マージンを考慮
        QPointF targetPosInView = viewBottomRight - QPointF(overlayTextItem->boundingRect().width() + 10,
                                                            overlayTextItem->boundingRect().height() + 10);

        QPointF targetPosInScene = mapToScene(targetPosInView.toPoint());
        overlayTextItem->setPos(targetPosInScene);
    }
}

main.cpp (使用例)

#include <QApplication>
#include <QMainWindow>
#include <QGraphicsScene>
#include <QGraphicsRectItem>
#include <QVBoxLayout>
#include <QWidget>
#include "mygraphicsview.h"

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

    QMainWindow window;
    window.setWindowTitle("QGraphicsView::scrollContentsBy() Example 2");

    QGraphicsScene *scene = new QGraphicsScene(-500, -500, 1000, 1000); // 広いシーン
    scene->addRect(-200, -200, 400, 400, QPen(Qt::blue), QBrush(Qt::lightGray));
    scene->addEllipse(50, 50, 100, 100, QPen(Qt::red), QBrush(Qt::cyan));
    scene->addText("Hello, Qt Graphics View!", QFont("Arial", 50));

    // オーバーレイアイテムはMyGraphicsViewのコンストラクタで追加される
    MyGraphicsView *view = new MyGraphicsView(scene);
    view->setDragMode(QGraphicsView::ScrollHandDrag); // マウスドラッグでスクロール可能にする

    QWidget *centralWidget = new QWidget;
    QVBoxLayout *layout = new QVBoxLayout(centralWidget);
    layout->addWidget(view);
    window.setCentralWidget(centralWidget);

    window.resize(600, 400);
    window.show();

    return a.exec();
}

この例では、overlayTextItem->setFlag(QGraphicsItem::ItemIgnoresTransformations, true); を設定しているため、ビューのズーム(拡大縮小)には影響されませんが、スクロールには setPos() で手動で追従させる必要があります。updateOverlayPosition() 関数で、ビューポートの右下隅に基づいてアイテムの位置を計算し、mapToScene() でシーン座標に変換して設定しています。

scrollContentsBy() を利用して、ビューのスクロールイベントを捕捉し、カスタムシグナルとして外部に通知する例です。これは、例えば複数の QGraphicsView を同期させたい場合などに役立ちます。

mygraphicsview.h (上記に加えて)

#ifndef MYGRAPHICSVIEW_H
#define MYGRAPHICSVIEW_H

#include <QGraphicsView>
#include <QDebug>

class MyGraphicsView : public QGraphicsView
{
    Q_OBJECT

public:
    explicit MyGraphicsView(QWidget *parent = nullptr);
    MyGraphicsView(QGraphicsScene *scene, QWidget *parent = nullptr);

signals:
    // カスタムスクロールシグナル
    void contentsScrolled(int dx, int dy);

protected:
    void scrollContentsBy(int dx, int dy) override;
};

#endif // MYGRAPHICSVIEW_H

mygraphicsview.cpp (上記に加えて)

#include "mygraphicsview.h"

MyGraphicsView::MyGraphicsView(QWidget *parent)
    : QGraphicsView(parent)
{
}

MyGraphicsView::MyGraphicsView(QGraphicsScene *scene, QWidget *parent)
    : QGraphicsView(scene, parent)
{
}

void MyGraphicsView::scrollContentsBy(int dx, int dy)
{
    QGraphicsView::scrollContentsBy(dx, dy);

    // カスタムシグナルを発行
    emit contentsScrolled(dx, dy);
    qDebug() << "MyGraphicsView::scrollContentsBy() called: dx =" << dx << ", dy =" << dy;
}

main.cpp (使用例)

#include <QApplication>
#include <QMainWindow>
#include <QGraphicsScene>
#include <QGraphicsRectItem>
#include <QVBoxLayout>
#include <QHBoxLayout> // 複数のビューを配置するため
#include <QWidget>
#include "mygraphicsview.h"
#include <QLabel>

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr) : QMainWindow(parent)
    {
        setWindowTitle("QGraphicsView::scrollContentsBy() Example 3");

        QGraphicsScene *scene1 = new QGraphicsScene(-500, -500, 1000, 1000);
        scene1->addRect(-200, -200, 400, 400, QPen(Qt::blue), QBrush(Qt::lightGray));
        scene1->addText("View 1", QFont("Arial", 50));

        QGraphicsScene *scene2 = new QGraphicsScene(-500, -500, 1000, 1000);
        scene2->addRect(-200, -200, 400, 400, QPen(Qt::green), QBrush(Qt::darkGray));
        scene2->addText("View 2", QFont("Arial", 50));

        MyGraphicsView *view1 = new MyGraphicsView(scene1);
        MyGraphicsView *view2 = new MyGraphicsView(scene2);

        view1->setDragMode(QGraphicsView::ScrollHandDrag);
        view2->setDragMode(QGraphicsView::ScrollHandDrag); // 両方をドラッグ可能にする

        // スクロールバーポリシーをオンにして、必ず表示されるようにする
        view1->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
        view1->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
        view2->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
        view2->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);

        // View1のスクロールにView2を同期させる
        // 通常はQScrollBarのvalueChangedシグナルを直接繋ぐ方が一般的ですが、
        // scrollContentsBy()経由での通知の例として示します。
        // より堅牢な同期のためには、QScrollBarの信号とスロットを使用してください。
        connect(view1, &MyGraphicsView::contentsScrolled, this, &MainWindow::syncViews);
        // 同期は片方向だけにして、再帰呼び出しを防ぐか、
        // QScrollBarの信号を接続するのが良いでしょう。
        // connect(view2, &MyGraphicsView::contentsScrolled, this, &MainWindow::syncViews); // これは無限ループになる可能性あり

        QLabel *statusLabel = new QLabel("スクロール状態: (0, 0)");

        QWidget *centralWidget = new QWidget;
        QVBoxLayout *mainLayout = new QVBoxLayout(centralWidget);
        QHBoxLayout *viewsLayout = new QHBoxLayout;
        viewsLayout->addWidget(view1);
        viewsLayout->addWidget(view2);
        mainLayout->addLayout(viewsLayout);
        mainLayout->addWidget(statusLabel);
        setCentralWidget(centralWidget);

        resize(800, 600);
    }

private slots:
    void syncViews(int dx, int dy)
    {
        // View1がスクロールしたときに、View2も同じ量だけスクロールさせる
        // ただし、この方法はあまり推奨されません。
        // 通常はQScrollBarの値を直接同期させるのがベストプラクティスです。
        // 例: view2->horizontalScrollBar()->setValue(view2->horizontalScrollBar()->value() + dx);
        // より正確には、ビューのスクロールバーの値を直接設定すべきです。
        qDebug() << "同期トリガー: dx =" << dx << ", dy =" << dy;

        // ここでは、現在のスクロール位置を取得し、dx, dyを適用して新しい位置を設定する
        MyGraphicsView* senderView = qobject_cast<MyGraphicsView*>(sender());
        if (!senderView) return;

        // senderViewがview1であると仮定して、view2を動かす
        QGraphicsView* targetView = nullptr;
        if (senderView == findChild<MyGraphicsView*>("MyGraphicsView_1")) { // 名前で特定する場合
            targetView = findChild<MyGraphicsView*>("MyGraphicsView_2");
        } else {
            // 他のビューからのスクロールの場合(必要であれば)
            targetView = findChild<MyGraphicsView*>("MyGraphicsView_1");
        }

        if (targetView) {
            QScrollBar* hBar = targetView->horizontalScrollBar();
            QScrollBar* vBar = targetView->verticalScrollBar();
            hBar->setValue(hBar->value() + dx);
            vBar->setValue(vBar->value() + dy);
        }

        // ラベルの更新
        QLabel *statusLabel = findChild<QLabel*>();
        if (statusLabel) {
            statusLabel->setText(QString("スクロール状態: (H: %1, V: %2)")
                                     .arg(senderView->horizontalScrollBar()->value())
                                     .arg(senderView->verticalScrollBar()->value()));
        }
    }
};

#include "main.moc" // Q_OBJECTがある場合、mocファイルが必要

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

    MainWindow window;
    // ビューにオブジェクト名を割り当てる(syncViewsで特定するため)
    window.findChildren<MyGraphicsView*>()[0]->setObjectName("MyGraphicsView_1");
    if (window.findChildren<MyGraphicsView*>().size() > 1) {
        window.findChildren<MyGraphicsView*>()[1]->setObjectName("MyGraphicsView_2");
    }

    window.show();

    return a.exec();
}

重要な注意
上記の syncViews 関数は、scrollContentsBy()dx, dy を使って直接スクロールバーの値を更新する例として示していますが、通常、複数の QGraphicsView のスクロールを同期させる最も堅牢な方法は、各 QGraphicsViewhorizontalScrollBar() および verticalScrollBar() が持つ valueChanged(int value) シグナルを直接接続することです。

// 複数のビューのスクロールバーを直接接続する例 (推奨される方法)
connect(view1->horizontalScrollBar(), &QScrollBar::valueChanged,
        view2->horizontalScrollBar(), &QScrollBar::setValue);
connect(view2->horizontalScrollBar(), &QScrollBar::valueChanged,
        view1->horizontalScrollBar(), &QScrollBar::setValue);

connect(view1->verticalScrollBar(), &QScrollBar::valueChanged,
        view2->verticalScrollBar(), &QScrollBar::setValue);
connect(view2->verticalScrollBar(), &QScrollBar::valueChanged,
        view1->verticalScrollBar(), &QScrollBar::setValue);

この方法だと、scrollContentsBy() をオーバーライドしてカスタムシグナルを出す必要がなく、より直接的で安全です。scrollContentsBy() は、あくまで「スクロールが発生したに何か追加で処理をしたい」場合に適しています。



QScrollBar の直接操作とシグナル/スロット

QGraphicsView は内部的に QScrollBar オブジェクトを使用してスクロールバーを管理しています。これらのスクロールバーは horizontalScrollBar()verticalScrollBar() メソッドでアクセスできます。スクロールバーの値を直接設定したり、その valueChanged() シグナルを接続したりすることで、細かくスクロールを制御できます。これは、特に複数のビューを同期させる場合に非常に強力な方法です。

メリット

  • scrollContentsBy() をオーバーライドするよりもシンプルになることが多い。
  • 複数のビューのスクロール同期に非常に適している。
  • 直接的で分かりやすいスクロール制御。

デメリット

  • ユーザーがマウスでドラッグした結果としての dx/dy のような「差分」ではなく、絶対的なスクロール位置を扱う必要がある。

使用例(複数のビューのスクロール同期)

#include <QApplication>
#include <QMainWindow>
#include <QGraphicsView>
#include <QGraphicsScene>
#include <QGraphicsRectItem>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QWidget>
#include <QScrollBar> // QScrollBarを使用

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

    QMainWindow window;
    window.setWindowTitle("Scroll Synchronization Example (using QScrollBar)");

    QGraphicsScene *scene1 = new QGraphicsScene(-500, -500, 1000, 1000);
    scene1->addRect(-200, -200, 400, 400, QPen(Qt::blue), QBrush(Qt::lightGray));
    scene1->addText("View 1", QFont("Arial", 50));

    QGraphicsScene *scene2 = new QGraphicsScene(-500, -500, 1000, 1000);
    scene2->addRect(-200, -200, 400, 400, QPen(Qt::green), QBrush(Qt::darkGray));
    scene2->addText("View 2", QFont("Arial", 50));

    QGraphicsView *view1 = new QGraphicsView(scene1);
    QGraphicsView *view2 = new QGraphicsView(scene2);

    view1->setDragMode(QGraphicsView::ScrollHandDrag);
    view2->setDragMode(QGraphicsView::ScrollHandDrag);

    // スクロールバーの値を相互に同期させる
    // View1の水平スクロールバーが変更されたら、View2の水平スクロールバーも変更する
    QObject::connect(view1->horizontalScrollBar(), &QScrollBar::valueChanged,
                     view2->horizontalScrollBar(), &QScrollBar::setValue);
    // View2の水平スクロールバーが変更されたら、View1の水平スクロールバーも変更する
    QObject::connect(view2->horizontalScrollBar(), &QScrollBar::valueChanged,
                     view1->horizontalScrollBar(), &QScrollBar::setValue);

    // 垂直スクロールバーも同様に同期させる
    QObject::connect(view1->verticalScrollBar(), &QScrollBar::valueChanged,
                     view2->verticalScrollBar(), &QScrollBar::setValue);
    QObject::connect(view2->verticalScrollBar(), &QScrollBar::valueChanged,
                     view1->verticalScrollBar(), &QScrollBar::setValue);

    QWidget *centralWidget = new QWidget;
    QHBoxLayout *layout = new QHBoxLayout(centralWidget);
    layout->addWidget(view1);
    layout->addWidget(view2);
    window.setCentralWidget(centralWidget);

    window.resize(800, 600);
    window.show();

    return a.exec();
}

centerOn(), ensureVisible(), fitInView() の使用

これらの関数は、特定のアイテムやシーンの領域にビューをプログラム的に移動させるための便利なメソッドです。手動でスクロールバーの値を計算する手間を省きます。

  • void QGraphicsView::fitInView(const QRectF &rect, Qt::AspectRatioMode aspectRatioMode) / (const QGraphicsItem *item, ...): 指定されたシーン矩形 rect、または指定された QGraphicsItem がビューポート内に収まるようにビューを拡大縮小(ズーム)し、必要に応じてスクロールします。アスペクト比を維持するかどうかを指定できます。
  • void QGraphicsView::ensureVisible(const QRectF &rect, int xmargin, int ymargin) / (const QGraphicsItem *item, ...): 指定されたシーン矩形 rect、または指定された QGraphicsItem がビューポート内に完全に収まるようにスクロールします。必要に応じてマージンを追加することもできます。
  • void QGraphicsView::centerOn(const QPointF &pos) / (const QGraphicsItem *item): 指定されたシーン座標 pos、または指定された QGraphicsItem の中心がビューポートの中心に来るようにスクロールします。

メリット

  • アニメーションと組み合わせて滑らかな移動を実現できる(例: QTimeLine を使用)。
  • 複雑な座標計算が不要。
  • 高レベルなAPIで、スクロールやズームの目的を簡単に達成できる。

デメリット

  • これらの関数が呼び出されたときに「スクロールが発生した」ことを検知するには、やはり scrollContentsBy() のオーバーライドやスクロールバーのシグナルを監視する必要がある。
  • scrollContentsBy() のように「スクロールの差分」を処理する汎用的なフックとしては使えない。

使用例(アイテムをクリックしたらそのアイテムを中央に表示)

#include <QApplication>
#include <QMainWindow>
#include <QGraphicsView>
#include <QGraphicsScene>
#include <QGraphicsRectItem>
#include <QMouseEvent> // マウスイベント用
#include <QDebug>

// QGraphicsView を継承してクリックイベントを処理するカスタムビュー
class MyClickableView : public QGraphicsView
{
    Q_OBJECT
public:
    explicit MyClickableView(QGraphicsScene *scene, QWidget *parent = nullptr)
        : QGraphicsView(scene, parent) {}

protected:
    void mousePressEvent(QMouseEvent *event) override
    {
        // クリックされた位置のアイテムを探す
        QGraphicsItem *item = itemAt(event->pos());
        if (item) {
            qDebug() << "アイテムがクリックされました。そのアイテムを中心に表示します。";
            centerOn(item); // クリックされたアイテムを中心に表示
        }
        QGraphicsView::mousePressEvent(event); // 基底クラスのイベントも処理
    }
};

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

    QMainWindow window;
    window.setWindowTitle("centerOn() Example");

    QGraphicsScene *scene = new QGraphicsScene(-500, -500, 1000, 1000); // 広いシーン
    scene->addRect(-200, -200, 400, 400, QPen(Qt::blue), QBrush(Qt::lightGray));
    QGraphicsRectItem *redRect = new QGraphicsRectItem(300, 300, 150, 150);
    redRect->setBrush(Qt::red);
    scene->addItem(redRect);
    scene->addText("クリックして中心に", QFont("Arial", 50));

    MyClickableView *view = new MyClickableView(scene);
    view->setDragMode(QGraphicsView::ScrollHandDrag);

    window.setCentralWidget(view);
    window.resize(600, 400);
    window.show();

    return a.exec();
}
#include "main.moc" // Q_OBJECTがある場合

QGraphicsView::setTransform() を使用したカスタムビュー変換

QGraphicsView は内部的に QTransform を使用してシーンの変換(平行移動、拡大縮小、回転など)を管理しています。setTransform() メソッドを使用してビューの変換行列を直接操作することで、スクロールを含め、より低レベルで柔軟なビューの移動を実現できます。

メリット

  • 非常に柔軟性が高い。
  • スクロールだけでなく、ズーム、回転、せん断など、あらゆるビューの変換を単一のメカニズムで制御できる。

デメリット

  • スクロールバーとの同期を自分で管理する必要がある場合がある。
  • QTransform の知識が必要で、centerOn()ensureVisible() などの高レベルAPIよりも複雑になる。

使用例(手動でビューを平行移動)

#include <QApplication>
#include <QMainWindow>
#include <QGraphicsView>
#include <QGraphicsScene>
#include <QGraphicsRectItem>
#include <QTransform>
#include <QWheelEvent> // マウスホイールイベント用
#include <QMouseEvent> // マウスドラッグ用
#include <QDebug>

class MyTransformView : public QGraphicsView
{
    Q_OBJECT
public:
    explicit MyTransformView(QGraphicsScene *scene, QWidget *parent = nullptr)
        : QGraphicsView(scene, parent)
    {
        // 変換の中心をビューポートの中心に設定(ズーム時など)
        setTransformationAnchor(AnchorUnderMouse); // マウスカーソルを中心にズーム

        // スクロールバーは表示しない(カスタムスクロールのため)
        setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
        setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
    }

protected:
    QPoint lastPanPoint; // マウスドラッグ開始点

    void mousePressEvent(QMouseEvent *event) override
    {
        if (event->button() == Qt::LeftButton) {
            lastPanPoint = event->pos();
        }
        QGraphicsView::mousePressEvent(event);
    }

    void mouseReleaseEvent(QMouseEvent *event) override
    {
        QGraphicsView::mouseReleaseEvent(event);
    }

    void mouseMoveEvent(QMouseEvent *event) override
    {
        if (event->buttons() & Qt::LeftButton) {
            QPointF delta = mapToScene(event->pos()) - mapToScene(lastPanPoint);
            // 現在のビューの変換を取得
            QTransform currentTransform = transform();
            // 現在の平行移動量を考慮して新しい平行移動量を計算
            currentTransform.translate(delta.x(), delta.y());
            setTransform(currentTransform); // 新しい変換を設定
            lastPanPoint = event->pos(); // 現在の点を次のドラッグの開始点にする
        }
        QGraphicsView::mouseMoveEvent(event);
    }

    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);
        }
    }
};

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

    QMainWindow window;
    window.setWindowTitle("setTransform() / Manual Pan Example");

    QGraphicsScene *scene = new QGraphicsScene(-500, -500, 1000, 1000);
    scene->addRect(-200, -200, 400, 400, QPen(Qt::blue), QBrush(Qt::lightGray));
    scene->addText("ドラッグでパン、ホイールでズーム", QFont("Arial", 40));

    MyTransformView *view = new MyTransformView(scene);

    window.setCentralWidget(view);
    window.resize(600, 400);
    window.show();

    return a.exec();
}
#include "main.moc" // Q_OBJECTがある場合

この例では、mouseMoveEvent 内でマウスドラッグを処理し、QTransform::translate() を使用してビューを移動させています。setTransformationAnchor() は、ズームの中心点を設定する際に重要です。

QGraphicsViewQAbstractScrollArea を継承しており、ほとんどのビューポートイベント(マウスイベント、ホイールイベント、ペイントイベントなど)は仮想関数 viewportEvent(QEvent *event) を通して処理されます。これをオーバーライドすることで、スクロールに関連する低レベルなイベントを捕捉し、独自の動作を実装できます。

メリット

  • 非常に低レベルな制御が可能。
  • すべてのビューポートイベントを一元的に処理できる。

デメリット

  • ほとんどの場合、mousePressEvent(), wheelEvent() などの専用のイベントハンドラをオーバーライドする方がシンプル。
  • イベントのタイプを自分で判別し、適切な処理をディスパッチする必要があるため、コードが複雑になりがち。

使用例(イベントの種類をログ出力する)

#include <QApplication>
#include <QMainWindow>
#include <QGraphicsView>
#include <QGraphicsScene>
#include <QGraphicsRectItem>
#include <QEvent> // QEventを使用
#include <QDebug>

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

protected:
    bool viewportEvent(QEvent *event) override
    {
        // 発生したイベントの種類をログ出力
        qDebug() << "viewportEvent: Type =" << event->type();

        // 必要に応じて特定のイベントを処理
        if (event->type() == QEvent::Wheel) {
            QWheelEvent *wheelEvent = static_cast<QWheelEvent*>(event);
            qDebug() << "  Wheel Event: Delta =" << wheelEvent->angleDelta().y();
            // ここで独自のズーム処理などを実装
            // wheelEvent->accept(); // イベントを処理済みとしてマークすることも可能
        }
        // ... その他のイベントタイプ ...

        // 必ず基底クラスのviewportEventを呼び出す
        return QGraphicsView::viewportEvent(event);
    }
};

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

    QMainWindow window;
    window.setWindowTitle("viewportEvent() Example");

    QGraphicsScene *scene = new QGraphicsScene(-500, -500, 1000, 1000);
    scene->addRect(-200, -200, 400, 400, QPen(Qt::blue), QBrush(Qt::lightGray));
    scene->addText("ビューポート上で何かしてみてください", QFont("Arial", 30));

    MyEventHandlingView *view = new MyEventHandlingView(scene);
    view->setDragMode(QGraphicsView::ScrollHandDrag); // マウスドラッグでスクロール可能

    window.setCentralWidget(view);
    window.resize(600, 400);
    window.show();

    return a.exec();
}
#include "main.moc" // Q_OBJECTがある場合

void QGraphicsView::scrollContentsBy() は、ビューポートがスクロールしたときに何かカスタムの「副作用」を実行したい場合に適したフックです。しかし、スクロール動作自体をプログラム的に制御したり、複数のビューを同期させたりする場合には、以下のような代替方法がより直接的で推奨される場合があります。

  • QAbstractScrollArea::viewportEvent(): すべてのビューポートイベントを捕捉したい場合の低レベルなフックですが、通常はより具体的なイベントハンドラ(例: mouseMoveEvent, wheelEvent)をオーバーライドする方が簡単です。
  • QGraphicsView::setTransform(): スクロールだけでなく、ズームや回転を含む、より複雑で低レベルなビューの変換を制御したい場合に適しています。
  • centerOn(), ensureVisible(), fitInView(): 特定のシーン領域やアイテムへの高レベルな移動やズーム操作に適しています。
  • QScrollBar の直接操作: 最も直接的なスクロール位置の制御と同期に適しています。