【Qt入門】QAbstractScrollArea::wheelEventを使ったカスタムスクロール実装例

2025-05-27

QAbstractScrollArea::wheelEvent(QWheelEvent *event) は、Qtのウィジェットクラスである QAbstractScrollArea仮想関数です。この関数は、マウスホイールが回転されたときに発生するイベントを処理するために使用されます。

  • QWheelEvent
    引数として渡される QWheelEvent *event オブジェクトには、マウスホイールの回転方向、回転量、イベント発生時のマウスカーソルの位置など、ホイールイベントに関する詳細な情報が含まれています。
  • 仮想関数
    virtual void wheelEvent(QWheelEvent *event) と定義されているため、この関数をオーバーライドすることで、Qtのイベントシステムがマウスホイールイベントを受け取ったときに、自動的にあなたの実装が呼び出されます。
  • 目的
    QAbstractScrollArea を継承するカスタムウィジェットで、マウスホイールの操作(スクロール)に対する独自の動作を実装するためにオーバーライド(再実装)されることを想定しています。

QAbstractScrollArea における wheelEvent の役割

QAbstractScrollArea は、スクロール可能な領域を提供する基底クラスです。これには、ビューポート(表示領域)と、水平・垂直のスクロールバーが含まれます。wheelEvent は、このスクロール動作と密接に関連しています。

  • オーバーライドの必要性
    • カスタムスクロール
      特定のアプリケーション要件に応じて、デフォルトのピクセル単位のスクロールではなく、特定の単位(例: 項目ごと、ページごと)でスクロールさせたい場合。
    • 特殊な挙動
      マウスホイールの回転がスクロール以外の動作(例: ズームイン/アウト、別の要素の切り替え)を引き起こすようにしたい場合。
    • イベントのフィルタリング/無視
      特定の条件下でマウスホイールイベントを無視したり、別のイベントハンドラに転送したりしたい場合。
  • デフォルトの動作
    QAbstractScrollArea のデフォルトの wheelEvent 実装は、通常、マウスホイールの回転に応じてスクロールバーの値を調整し、ビューポートの内容をスクロールさせます。つまり、特別な実装を行わなくても、マウスホイールでコンテンツがスクロールするように機能します。

wheelEvent をオーバーライドする際の一般的な手順

  1. クラスの継承
    QAbstractScrollArea またはその派生クラス(例: QScrollArea, QTextEdit)を継承して新しいクラスを作成します。

  2. wheelEvent の再実装
    新しいクラスで wheelEvent 関数をオーバーライドします。

    #include <QAbstractScrollArea>
    #include <QWheelEvent>
    #include <QDebug> // デバッグ出力用
    
    class MyCustomScrollArea : public QAbstractScrollArea
    {
        Q_OBJECT
    public:
        explicit MyCustomScrollArea(QWidget *parent = nullptr) : QAbstractScrollArea(parent)
        {
            // ここでビューポートなどを設定できます
        }
    
    protected:
        void wheelEvent(QWheelEvent *event) override
        {
            // マウスホイールの回転方向と量をデバッグ出力
            qDebug() << "Wheel event: delta =" << event->angleDelta().y();
    
            // ここにカスタムのスクロールロジックを記述します。
            // 例えば、垂直スクロールバーの値を変更する:
            // int delta = event->angleDelta().y(); // 通常は120の倍数
            // verticalScrollBar()->setValue(verticalScrollBar()->value() - delta);
    
            // あるいは、特定の条件で親クラスのデフォルト動作を呼び出す:
            // if (event->modifiers() == Qt::NoModifier) {
            //     QAbstractScrollArea::wheelEvent(event); // デフォルトのスクロール動作を呼び出す
            // } else {
            //     // Modifierが押されている場合は、別の処理を行う
            //     qDebug() << "Modifier key pressed, not scrolling.";
            //     event->accept(); // イベントを処理済みとしてマーク
            // }
    
            // イベントを処理したことを示すために、event->accept() を呼び出すことが推奨されます。
            // デフォルトの動作を呼び出した場合、通常はevent->accept()も呼び出されます。
            // ここでは、デフォルトの動作を呼び出すか、自分で処理を行うかによって調整します。
            // 今回はデバッグ出力のみなので、デフォルトの動作は呼び出さないでおきます。
            event->accept(); // イベントを処理済みとしてマークし、親ウィジェットへの伝播を防ぐ
        }
    };
    

QWheelEvent の主な情報

  • event->pos() / event->globalPos(): イベント発生時のマウスカーソルのローカル座標およびグローバル座標。
  • event->modifiers(): イベント発生時のキーボード修飾キー(Shift, Ctrl, Altなど)の状態。
  • event->buttons(): イベント発生時のマウスボタンの状態。
  • event->pixelDelta(): デバイスに依存しないピクセル単位のスクロール量。通常、よりスムーズなスクロールを実現するために使用されます。
  • event->angleDelta().x(): 水平方向のホイール回転量。一部のホイールやトラックパッドで利用可能です。
  • event->angleDelta().y(): 垂直方向のホイール回転量(通常は120の倍数)。正の値は上方向(前方)へのスクロール、負の値は下方向(後方)へのスクロールを示します。

注意点

  • viewportEvent()
    QAbstractScrollArea は、ビューポートで発生するすべてのイベントを viewportEvent() 仮想関数にマッピングします。wheelEvent もその一つであり、内部的には viewportEvent を通じて処理されます。しかし、ほとんどの場合、wheelEvent を直接オーバーライドする方が簡潔です。
  • event->accept(): wheelEvent をオーバーライドした場合、イベントを完全に処理した場合は event->accept() を呼び出すことが重要です。これにより、イベントが親ウィジェットに伝播するのを防ぎます。もしイベントを処理せず、親ウィジェットが処理できるようにしたい場合は event->ignore() を呼び出します(ただし、Qtのイベント伝播の仕組み上、通常は明示的に ignore() を呼び出す必要はあまりありません)。


wheelEvent() が全く呼び出されない/期待通りに呼び出されない

よくある原因

  • フォーカスがない
    ホイールイベントは、マウスカーソルがウィジェット上にあるか、ウィジェットがフォーカスを持っている場合に処理されます。ウィジェットがフォーカスを持っていない、またはマウスカーソルが別のウィジェット上にある場合、イベントは発生しません。
  • イベントの伝播の遮断
    • 子ウィジェットによるイベント消費
      QAbstractScrollArea の内部にある子ウィジェットが wheelEvent を受け取り、event->accept() を呼び出してイベントを消費してしまっている場合、親である QAbstractScrollArea はイベントを受け取ることができません。特に、QGraphicsView のような複雑なビューを内部に持つ場合によく発生します。
    • 別のイベントフィルターがイベントをブロックしている
      アプリケーションや親ウィジェットにインストールされたイベントフィルターが、wheelEvent を途中で処理してしまい、目的の QAbstractScrollArea に到達しない場合があります。

トラブルシューティング

  1. event->accept() / event->ignore() の確認
    • wheelEvent() をオーバーライドした際に、最後に event->accept() を呼び出していますか? これにより、イベントが処理済みであることをQtに伝え、それ以上親ウィジェットに伝播するのを防ぎます。
    • もし、子ウィジェットがイベントを消費している疑いがある場合、その子ウィジェットの wheelEvent() またはイベントフィルターで event->ignore() を呼び出し、親ウィジェットにイベントを転送するように設定してみます。
  2. イベントフィルターの確認
    • アプリケーション全体、または関連する親ウィジェットにカスタムのイベントフィルターがインストールされていないか確認します。もしあれば、そのフィルター内で wheelEvent がどのように扱われているかを確認し、必要に応じて変更します。
  3. 子のイベント処理をオーバーライドする
    • QAbstractScrollAreaviewport() に設定されているウィジェット(通常は QWidget を継承したもの)の wheelEvent() をオーバーライドし、そこでイベントを処理するか、または明示的に event->ignore() を呼び出して親に転送するようにします。
  4. Qt::AA_CompressHighFrequencyEvents の設定
    • アプリケーションの起動時に QApplication::setAttribute(Qt::AA_CompressHighFrequencyEvents, false); を設定して、高頻度イベントの圧縮を無効にしてみます。これにより、ホイールイベントがより頻繁に、そして個別に報告されるようになる可能性があります。ただし、パフォーマンスへの影響も考慮してください。
  5. マウスカーソルの位置とフォーカス
    • ホイールイベントをテストする際、必ず目的の QAbstractScrollArea またはそのビューポート上にマウスカーソルがあることを確認してください。
    • ウィジェットがフォーカスを取得する必要がある場合は、setFocusPolicy(Qt::WheelFocus)setFocusPolicy(Qt::StrongFocus) などを設定し、プログラム的に setFocus() を呼び出すことを検討します。

スクロールが期待通りに動作しない

よくある原因

  • デフォルトの wheelEvent 呼び出し忘れ
    • カスタムロジックを実装した後、親クラスの QAbstractScrollArea::wheelEvent(event); を呼び出していない場合、Qtのデフォルトのスクロールバー処理が行われません。カスタムのスクロール動作を追加したいが、既存のスクロール動作も維持したい場合に問題となります。
  • スクロールバーの値の更新ロジックの誤り
    • スクロールバーの value() を直接操作する場合、その計算ロジックが間違っていると、スクロールが正しく行われないことがあります。
    • verticalScrollBar()->setValue(...)horizontalScrollBar()->setValue(...) を使用する場合、最小値、最大値、ページステップなどを考慮する必要があります。
  • event->angleDelta() と event->pixelDelta() の混同
    • angleDelta() はホイールの「クリック」または「ノッチ」単位の回転量(通常、120の倍数)を返します。
    • pixelDelta() は高精度なトラックパッドなどで利用可能なピクセル単位のスクロール量です。
    • プラットフォームやデバイスによってどちらが適切かが異なります。特に、古いシステムや一部のマウスでは angleDelta() が標準的ですが、macOSのトラックパッドなどでは pixelDelta() の方がスムーズなスクロールを実現します。これらを混同すると、スクロール量が大きすぎたり小さすぎたりすることがあります。

トラブルシューティング

  1. angleDelta() と pixelDelta() の使い分け
    • event->angleDelta().y() は、一般的なマウスホイールイベントで信頼性の高い値を提供します。
    • event->pixelDelta().y() は、よりスムーズなスクロールが必要な場合や、高精度な入力デバイスに対応する場合に検討しますが、source()Qt::MouseEventSynthesizedBySystem であることを確認すると良いでしょう。
    • 両方を組み合わせて、プラットフォームに依存しないスムーズなスクロールを実現することも可能です。
    int delta = 0;
    if (event->pixelDelta().y() != 0) {
        delta = event->pixelDelta().y();
    } else {
        delta = event->angleDelta().y();
    }
    // delta を使ってスクロール処理を行う
    
  2. スクロールバーの範囲とステップ
    • verticalScrollBar()->minimum(), verticalScrollBar()->maximum(), verticalScrollBar()->pageStep() などのプロパティを考慮して、スクロールバーの値を適切に更新しているか確認します。
    • verticalScrollBar()->setSliderPosition(new_value); のように、直接スライダー位置を設定することもできます。
  3. 親クラスの wheelEvent() の呼び出し
    • カスタムの処理を行った後に、デフォルトのスクロール動作も必要であれば、QAbstractScrollArea::wheelEvent(event); を呼び出します。ただし、イベントが二重に処理されないように注意が必要です。例えば、自分で完全にイベントを処理する場合は event->accept() を呼び出し、親のデフォルト動作は呼び出しません。
    • もし、カスタムロジックでスクロールバーを直接操作している場合は、親の wheelEvent() を呼び出す必要はありません。

よくある原因

  • wheelEvent 内での別のイベントの発行
    • wheelEvent の中で、再びホイールイベントを引き起こすような処理(例: スクロールバーの値を変更するシグナルが別のイベントをトリガーする)を行っている場合、無限ループに近い状態になる可能性があります。
  • update() や viewport()->update() の過剰な呼び出し
    • wheelEvent() 内で update()viewport()->update() を頻繁に呼び出しすぎると、不必要な再描画が発生し、パフォーマンスが低下したり、UIの応答性が悪くなったりすることがあります。
  1. 必要な部分のみを更新
    • update() の代わりに viewport()->update(rect) のように、変更があった領域のみを更新するようにします。
  2. フラグによる制御
    • イベント処理中に一時的にフラグを立てて、再帰的なイベント処理を防ぐなどの工夫をします。
  3. 再描画頻度の制御
    • アニメーションなど、滑らかな動きが必要な場合は、QTimerなどを使って一定間隔で update() を呼び出すようにし、イベントハンドラ内では直接 update() を呼び出さないようにします。


例1: デフォルトのスクロール動作に加えて、デバッグ情報を出力する

この例では、QAbstractScrollArea を継承したカスタムウィジェットを作成し、マウスホイールが回転したときに、そのイベントの詳細をデバッグ出力します。同時に、QAbstractScrollArea のデフォルトのスクロール動作も維持します。

// customscrollarea.h
#ifndef CUSTOMSCROLLAREA_H
#define CUSTOMSCROLLAREA_H

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

class CustomScrollArea : public QAbstractScrollArea
{
    Q_OBJECT
public:
    explicit CustomScrollArea(QWidget *parent = nullptr) : QAbstractScrollArea(parent)
    {
        // スクロールエリアのビューポートに表示するダミーのウィジェットを設定
        // 実際のアプリケーションでは、ここに表示したいコンテンツ(例: QWidget, QGraphicsViewなど)を設定します。
        QWidget *contentWidget = new QWidget(this);
        contentWidget->setMinimumSize(800, 800); // スクロール可能にするために十分なサイズを設定
        contentWidget->setStyleSheet("background-color: lightblue; border: 2px solid darkblue;"); // 見た目を分かりやすくする
        setViewport(contentWidget);
    }

protected:
    // QAbstractScrollArea::wheelEvent をオーバーライド
    void wheelEvent(QWheelEvent *event) override
    {
        // 1. イベントのデバッグ情報を出力
        qDebug() << "--- Wheel Event Captured ---";
        qDebug() << "  Angle Delta Y:" << event->angleDelta().y(); // ホイールのノッチ単位の回転量(垂直方向)
        qDebug() << "  Angle Delta X:" << event->angleDelta().x(); // ホイールのノッチ単位の回転量(水平方向)
        qDebug() << "  Pixel Delta Y:" << event->pixelDelta().y(); // ピクセル単位の回転量(垂直方向、高精度デバイス向け)
        qDebug() << "  Pixel Delta X:" << event->pixelDelta().x(); // ピクセル単位の回転量(水平方向、高精度デバイス向け)
        qDebug() << "  Buttons:" << event->buttons();             // イベント発生時のマウスボタン
        qDebug() << "  Modifiers:" << event->modifiers();         // イベント発生時の修飾キー (Ctrl, Shiftなど)
        qDebug() << "  Position (Local):" << event->pos();       // ウィジェットローカル座標
        qDebug() << "  Position (Global):" << event->globalPos(); // スクリーン座標
        qDebug() << "--------------------------";

        // 2. 親クラス(QAbstractScrollArea)のデフォルトの wheelEvent を呼び出す
        //    これにより、Qtの標準的なスクロール動作(スクロールバーの移動など)が行われます。
        QAbstractScrollArea::wheelEvent(event);

        // 注意: 親クラスの wheelEvent が既に event->accept() を呼び出すため、
        //      通常はここで再度 event->accept() を呼び出す必要はありません。
    }
};

#endif // CUSTOMSCROLLAREA_H
// main.cpp
#include <QApplication>
#include <QMainWindow>
#include "customscrollarea.h"

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

    QMainWindow window;
    CustomScrollArea *scrollArea = new CustomScrollArea(&window);
    window.setCentralWidget(scrollArea);
    window.setWindowTitle("Custom Scroll Area with Wheel Event Logging");
    window.resize(600, 400);
    window.show();

    return a.exec();
}

解説

  • 最も重要な点は、QAbstractScrollArea::wheelEvent(event); を呼び出していることです。これにより、カスタムのデバッグ出力に加えて、親クラスが持つ標準のスクロール処理(マウスホイールの回転に応じたスクロールバーの移動)が実行されます。
  • wheelEvent(QWheelEvent *event) をオーバーライドし、その中で QWheelEvent オブジェクトから様々な情報を取得し、qDebug() で出力しています。
  • コンストラクタで、スクロールエリア内に表示するダミーの QWidgetsetViewport() で設定しています。これにより、スクロールバーが表示され、スクロール可能になります。
  • CustomScrollArea クラスは QAbstractScrollArea を継承しています。

この例では、QAbstractScrollArea の中に画像を表示し、マウスホイールを使って画像をズームイン/ズームアウトする機能を追加します。デフォルトのスクロール動作は行いません。

// zoomableimagearea.h
#ifndef ZOOMABLEIMAGEAREA_H
#define ZOOMABLEIMAGEAREA_H

#include <QAbstractScrollArea>
#include <QWheelEvent>
#include <QPainter>
#include <QPixmap>
#include <QDebug>

class ZoomableImageArea : public QAbstractScrollArea
{
    Q_OBJECT
public:
    explicit ZoomableImageArea(QWidget *parent = nullptr) : QAbstractScrollArea(parent)
    {
        // 画像をロード
        // 実際のパスに置き換えてください。テスト用に一時的な画像を使用することもできます。
        // 例: QPixmap(":/images/your_image.png");
        m_pixmap.load(":/images/sample_image.jpg"); // プロジェクトにリソースファイルを追加し、画像パスを指定
        if (m_pixmap.isNull()) {
            qWarning() << "Failed to load image! Make sure the path is correct and image exists.";
            // ダミーの画像を生成
            m_pixmap = QPixmap(200, 200);
            m_pixmap.fill(Qt::red);
            QPainter painter(&m_pixmap);
            painter.setPen(Qt::black);
            painter.drawText(m_pixmap.rect(), Qt::AlignCenter, "Image not found!");
        }

        m_scaleFactor = 1.0; // 初期ズーム倍率
        setViewport(new QWidget(this)); // ダミーのビューポートを設定
        viewport()->setBackgroundRole(QPalette::Dark); // 背景色を設定
        viewport()->setAutoFillBackground(true);
        updateScrollBars(); // スクロールバーの範囲を更新
    }

protected:
    void wheelEvent(QWheelEvent *event) override
    {
        // Ctrlキーが押されている場合にのみズーム操作を行う
        if (event->modifiers() & Qt::ControlModifier) {
            double numDegrees = event->angleDelta().y() / 8.0; // 1クリックあたりの角度を度数に変換
            double numSteps = numDegrees / 15.0;                // 通常のホイールステップ数に変換 (1ステップ=15度)

            // ズーム倍率を調整
            m_scaleFactor += numSteps * 0.1; // 0.1 はズーム速度を調整する係数
            if (m_scaleFactor < 0.1) m_scaleFactor = 0.1; // 最小ズーム倍率を設定
            if (m_scaleFactor > 5.0) m_scaleFactor = 5.0; // 最大ズーム倍率を設定

            qDebug() << "Zooming: New Scale Factor =" << m_scaleFactor;

            updateScrollBars(); // スクロールバーの範囲を更新
            viewport()->update(); // ビューポートを再描画
            event->accept(); // イベントを処理済みとしてマークし、親への伝播を防ぐ
        } else {
            // Ctrlキーが押されていない場合は、デフォルトのスクロール動作(QAbstractScrollAreaの)を呼び出す
            QAbstractScrollArea::wheelEvent(event);
        }
    }

    void paintEvent(QPaintEvent *event) override
    {
        // ビューポートの描画は、paintEventではなく viewportEvent() が呼ばれることが多いですが、
        // QAbstractScrollArea の派生クラスで直接コンテンツを描画する場合は、
        // viewport() の paintEvent をオーバーライドするか、このクラスで直接処理します。
        // ここでは、paintEventをオーバーライドして描画しています。
        // しかし、QAbstractScrollAreaは通常、setViewport()で設定したウィジェットの描画を委譲するため、
        // この方法で描画する場合は、viewport()->paintEvent(event); を呼び出すか、
        // viewport()->paintEvent()をオーバーライドして描画ロジックを実装するのが一般的です。
        // 簡単のために、ここでは QAbstractScrollArea の paintEvent で直接描画しています。

        QPainter painter(viewport()); // ビューポートに描画
        painter.setRenderHint(QPainter::Antialiasing);
        painter.setRenderHint(QPainter::SmoothPixmapTransform);

        // スクロールオフセットを考慮
        QPoint offset = -contentOffset();

        // ズームとオフセットを適用して画像を描画
        painter.translate(offset);
        painter.scale(m_scaleFactor, m_scaleFactor);
        painter.drawPixmap(0, 0, m_pixmap);

        QAbstractScrollArea::paintEvent(event); // 親クラスの描画も呼び出す(スクロールバーなど)
    }

    void updateScrollBars()
    {
        QSize contentSize = m_pixmap.size() * m_scaleFactor; // ズーム後のコンテンツサイズ

        horizontalScrollBar()->setRange(0, contentSize.width() - viewport()->width());
        verticalScrollBar()->setRange(0, contentSize.height() - viewport()->height());

        // スクロールバーのページステップを調整(ビューポートサイズに合わせる)
        horizontalScrollBar()->setPageStep(viewport()->width());
        verticalScrollBar()->setPageStep(viewport()->height());
    }

private:
    QPixmap m_pixmap;
    double m_scaleFactor;
};

#endif // ZOOMABLEIMAGEAREA_H
// main.cpp (例1と同じ)
#include <QApplication>
#include <QMainWindow>
#include "zoomableimagearea.h" // ヘッダーをCustomScrollAreaから変更

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

    // リソースファイルをロードするために必要(例: sample_image.jpg)
    // .proファイルに `RESOURCES += image.qrc` のように追加し、qrcファイルを作成する必要があります。
    // qrcファイルの内容例:
    // <RCC>
    //   <qresource prefix="/images">
    //     <file>sample_image.jpg</file>
    //   </qresource>
    // </RCC>

    QMainWindow window;
    ZoomableImageArea *imageArea = new ZoomableImageArea(&window);
    window.setCentralWidget(imageArea);
    window.setWindowTitle("Zoomable Image Area");
    window.resize(800, 600);
    window.show();

    return a.exec();
}

sample_image.jpg をプロジェクトに追加する方法

  1. プロジェクトディレクトリ内に適当な画像を配置します(例: sample_image.jpg)。
  2. Qt Creatorで、プロジェクトを右クリックし、「新規ファイルの追加」を選択します。
  3. 「Qt」カテゴリから「Qt リソースファイル」を選択し、ファイル名(例: image.qrc)を付けて作成します。
  4. image.qrc ファイルを開き、「追加」→「プレフィックスの追加」を選択し、images などと入力します。
  5. 作成したプレフィックスを選択し、「追加」→「ファイルの追加」を選択して、sample_image.jpg を追加します。
  6. main.cpp のコメントアウトされた部分を解除し、QPixmap(":/images/sample_image.jpg") のように記述します。
  • updateScrollBars() は、画像の現在のズーム倍率に基づいて、水平および垂直スクロールバーの範囲を動的に調整します。
  • paintEvent メソッドをオーバーライドし、ズーム倍率とスクロールオフセットを考慮して画像をビューポートに描画しています。
  • wheelEvent メソッドをオーバーライドしています。
    • if (event->modifiers() & Qt::ControlModifier) で、Ctrlキーが押されている場合にのみズーム操作を行うようにしています。これにより、ユーザーは通常のスクロールとズームを使い分けることができます。
    • event->angleDelta().y() を使ってホイールの回転量を取得し、それに応じて m_scaleFactor を更新しています。
    • updateScrollBars() を呼び出して、ズームによってコンテンツのサイズが変わるため、スクロールバーの範囲を再計算しています。
    • viewport()->update() を呼び出して、ビューポートを再描画させ、ズーム後の画像を表示します。
    • event->accept(); が重要です。 これにより、このイベントは ZoomableImageArea で完全に処理され、親クラスの QAbstractScrollArea が持つデフォルトのスクロール動作(この例ではズーム機能と競合するため)は呼び出されません。
    • Ctrlキーが押されていない場合は、QAbstractScrollArea::wheelEvent(event); を呼び出し、デフォルトのスクロール動作(画像がビューポートに収まらない場合にスクロール)を許可しています。
  • ZoomableImageAreaQAbstractScrollArea を継承し、内部に QPixmap を保持します。


イベントフィルター (Event Filters)

Qtのイベントシステムにおいて、イベントフィルターは非常に強力なメカニズムです。特定のオブジェクトが受け取る前に、別のオブジェクトがイベントをインターセプトして処理できます。

特徴

  • 既存ウィジェットへの適用
    既存のQtウィジェット(例: QTextEdit, QTableView など、wheelEvent を持たない、またはデフォルトの動作を変更したいウィジェット)の動作を変更する際に特に便利です。
  • 階層的制御
    親ウィジェットが子ウィジェットのイベントを監視・変更したり、その逆も可能です。
  • 柔軟性
    特定のウィジェットだけでなく、アプリケーション全体、あるいは特定のオブジェクトの子孫全てに対してイベントをフィルタリングできます。

使い方

  1. イベントフィルターを実装するクラス(通常は QObject のサブクラス)を作成し、eventFilter(QObject *watched, QEvent *event) 仮想関数をオーバーライドします。
  2. eventFilter メソッド内で、event->type() をチェックして QEvent::Wheel であるかを確認し、qobject_cast<QWheelEvent*>(event)QWheelEvent にキャストします。
  3. 必要な処理を行った後、イベントを処理済みとしてマークする場合は true を返し、イベントを通常の処理フローに沿って続行させたい場合は false を返します。
  4. ターゲットとなるウィジェットに対して、installEventFilter(this); を呼び出してイベントフィルターをインストールします。

コード例

// mywheelfilter.h
#ifndef MYWHEELFILTER_H
#define MYWHEELFILTER_H

#include <QObject>
#include <QEvent>
#include <QWheelEvent>
#include <QDebug>
#include <QAbstractScrollArea> // QAbstractScrollArea のスクロールバーを操作するために必要

class MyWheelFilter : public QObject
{
    Q_OBJECT
public:
    explicit MyWheelFilter(QObject *parent = nullptr) : QObject(parent) {}

protected:
    bool eventFilter(QObject *watched, QEvent *event) override
    {
        if (event->type() == QEvent::Wheel) {
            QWheelEvent *wheelEvent = static_cast<QWheelEvent*>(event);

            // イベントをフィルタリングしたいウィジェットが QAbstractScrollArea の派生クラスであるかを確認
            QAbstractScrollArea *scrollArea = qobject_cast<QAbstractScrollArea*>(watched);

            if (scrollArea) {
                // デバッグ情報を出力
                qDebug() << "Event Filter: Wheel event on" << watched->objectName();
                qDebug() << "  Delta Y:" << wheelEvent->angleDelta().y();

                // 例: Shiftキーが押されている場合のみ水平スクロールを行う
                if (wheelEvent->modifiers() & Qt::ShiftModifier) {
                    QScrollBar *hScrollBar = scrollArea->horizontalScrollBar();
                    if (hScrollBar) {
                        hScrollBar->setValue(hScrollBar->value() - wheelEvent->angleDelta().y());
                        wheelEvent->accept(); // イベントを処理済みとしてマーク
                        return true; // イベントを消費し、これ以上伝播させない
                    }
                } else {
                    // Shiftキーが押されていない場合は、通常の垂直スクロールを許可する
                    // イベントを続行させるため、falseを返す(または親クラスのwheelEventを呼び出す)
                    // ここで false を返すと、QAbstractScrollArea の wheelEvent() が呼ばれます。
                    // もしくは、ここで QAbstractScrollArea::wheelEvent() を明示的に呼び出すことも可能です。
                    // 例: QCoreApplication::sendEvent(watched, wheelEvent);
                }
            }
            // 処理しない、または処理を親に委譲したい場合は false を返す
            return false;
        }
        // 他のイベントは通常通り処理
        return QObject::eventFilter(watched, event);
    }
};

#endif // MYWHEELFILTER_H
// main.cpp (CustomScrollAreaの代わりにQScrollAreaを使用)
#include <QApplication>
#include <QMainWindow>
#include <QScrollArea>
#include <QLabel>
#include <QVBoxLayout>
#include "mywheelfilter.h"

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

    QMainWindow window;
    QScrollArea *scrollArea = new QScrollArea(&window);
    scrollArea->setObjectName("MyScrollArea"); // オブジェクト名を設定

    // スクロール可能なコンテンツを作成
    QWidget *contentWidget = new QWidget(scrollArea);
    QVBoxLayout *layout = new QVBoxLayout(contentWidget);
    for (int i = 0; i < 50; ++i) {
        QLabel *label = new QLabel(QString("Item %1").arg(i), contentWidget);
        label->setAlignment(Qt::AlignCenter);
        label->setMinimumHeight(30);
        layout->addWidget(label);
    }
    contentWidget->setMinimumSize(300, 1500); // スクロール可能にするために十分なサイズ
    contentWidget->setLayout(layout);
    scrollArea->setWidget(contentWidget);

    // イベントフィルターをインストール
    MyWheelFilter *filter = new MyWheelFilter(&a); // アプリケーションオブジェクトを親にすることも可能
    scrollArea->installEventFilter(filter);

    window.setCentralWidget(scrollArea);
    window.setWindowTitle("Event Filter Wheel Demo");
    window.resize(400, 300);
    window.show();

    return a.exec();
}

QScroller (Qt Widgets and Qt Quick)

QScroller クラスは、タッチイベントやマウスのドラッグ操作による「慣性スクロール」(フリックによるスクロール)を実装するためのクラスです。通常のホイールイベントとは少し異なりますが、スクロール動作をカスタマイズする文脈で考慮されることがあります。

特徴

  • 低レベルイベントの抽象化
    スクロールに関連する複雑な低レベルイベント処理を抽象化します。
  • 統一されたスクロール体験
    タッチスクリーンとマウス操作の両方で、より自然なスクロール体験を提供します。
  • 慣性スクロール
    ユーザーがコンテンツを「投げる」ようにフリックすると、慣性によってスクロールが続く動作を簡単に実装できます。

使い方

  1. QScroller::grabGesture() を使用して、スクロールさせたいウィジェットやグラフィックスアイテムにジェスチャーを関連付けます。
  2. ターゲットオブジェクトは、QEvent::ScrollPrepareQEvent::Scroll イベントを受け取るように、event() メソッドをオーバーライドする必要があります。
  3. QScrollPrepareEvent でスクロール範囲や現在の位置を設定し、QScrollEvent で実際にコンテンツを移動させる(描画を更新する)処理を行います。

コード例 (概念のみ、完全な実装は複雑)

#include <QApplication>
#include <QMainWindow>
#include <QGraphicsView>
#include <QGraphicsScene>
#include <QGraphicsRectItem>
#include <QScroller>
#include <QScrollPrepareEvent>
#include <QScrollEvent>
#include <QDebug>

// QGraphicsView を継承し、QScroller を使用する例
class MyGraphicsView : public QGraphicsView
{
    Q_OBJECT
public:
    explicit MyGraphicsView(QWidget *parent = nullptr) : QGraphicsView(parent)
    {
        QGraphicsScene *scene = new QGraphicsScene(this);
        scene->setSceneRect(0, 0, 2000, 1500); // 広いシーン
        scene->setBackgroundBrush(Qt::lightGray);
        scene->addItem(new QGraphicsRectItem(100, 100, 500, 500, nullptr, scene));
        setScene(scene);

        // QScroller でジェスチャーを有効にする
        QScroller::grabGesture(this, QScroller::LeftMouseButtonGesture); // マウスドラッグでフリック可能に
        // QScroller::grabGesture(this, QScroller::TouchGesture); // タッチイベントでもフリック可能に
    }

protected:
    bool event(QEvent *event) override
    {
        if (event->type() == QEvent::ScrollPrepare) {
            qDebug() << "ScrollPrepare Event";
            QScrollPrepareEvent *se = static_cast<QScrollPrepareEvent *>(event);

            se->setViewportSize(viewport()->size()); // ビューポートのサイズ
            se->setContentPos(mapToScene(QPoint(0,0))); // 現在のコンテンツ位置
            se->setContentPosRange(QRectF(scene()->sceneRect().topLeft(), scene()->sceneRect().bottomRight() - viewport()->size())); // スクロール可能範囲
            se->setScrollPoint(mapToScene(se->startPos())); // スクロール開始点

            se->accept(); // イベントを処理済みとしてマーク
            return true;
        } else if (event->type() == QEvent::Scroll) {
            qDebug() << "Scroll Event";
            QScrollEvent *se = static_cast<QScrollEvent *>(event);

            // 新しいスクロール位置にビューを移動
            centerOn(se->contentPos()); // シーンの指定された点をビューの中心に持ってくる

            se->accept(); // イベントを処理済みとしてマーク
            return true;
        }
        return QGraphicsView::event(event); // その他のイベントは親クラスに委譲
    }

    // 必要に応じてwheelEventもオーバーライドして、QScrollerと共存させるか、無効にするか決める
    void wheelEvent(QWheelEvent *event) override {
        // 例: QScrollerでスクロールしているので、wheelEventは無視するか、ズームなどに使う
        if (event->modifiers() & Qt::ControlModifier) {
            // ズーム処理など
            qDebug() << "Ctrl + Wheel for Zoom";
            event->accept();
        } else {
            // デフォルトのホイールスクロールも有効にする場合
            QGraphicsView::wheelEvent(event);
        }
    }
};

// main.cpp (MyGraphicsView を使用)
// #include <QApplication>
// #include <QMainWindow>
// #include "mygraphicsview.h"
//
// int main(int argc, char *argv[])
// {
//     QApplication a(argc, argv);
//     QMainWindow window;
//     MyGraphicsView *view = new MyGraphicsView(&window);
//     window.setCentralWidget(view);
//     window.setWindowTitle("QScroller Graphics View");
//     window.resize(800, 600);
//     window.show();
//     return a.exec();
// }

QScrollBar のシグナル/スロットを直接操作する

QAbstractScrollArea は内部的に QScrollBar オブジェクト(horizontalScrollBar()verticalScrollBar() でアクセス可能)を保持しています。これらのスクロールバーのシグナル(例: valueChanged(), sliderMoved(), actionTriggered())を直接接続し、カスタムロジックを実行することも可能です。

特徴

  • 精密な制御
    スクロールバーの最小値、最大値、ステップサイズなどを直接操作できます。
  • スクロールバー駆動
    イベントではなく、スクロールバーの値の変更に基づいて動作を制御します。

使い方

  1. horizontalScrollBar()verticalScrollBar() を介して QScrollBar オブジェクトを取得します。
  2. その QScrollBar オブジェクトのシグナルをカスタムスロットに接続します。
  3. スロット内でスクロールバーの新しい値に基づいてコンテンツを更新します。
#include <QApplication>
#include <QMainWindow>
#include <QScrollArea>
#include <QLabel>
#include <QVBoxLayout>
#include <QScrollBar> // QScrollBar クラスが必要
#include <QDebug>

class CustomScrollAreaWithScrollBarSignals : public QScrollArea
{
    Q_OBJECT
public:
    explicit CustomScrollAreaWithScrollBarSignals(QWidget *parent = nullptr) : QScrollArea(parent)
    {
        QWidget *contentWidget = new QWidget(this);
        QVBoxLayout *layout = new QVBoxLayout(contentWidget);
        for (int i = 0; i < 50; ++i) {
            QLabel *label = new QLabel(QString("Item %1").arg(i), contentWidget);
            label->setAlignment(Qt::AlignCenter);
            label->setMinimumHeight(30);
            layout->addWidget(label);
        }
        contentWidget->setMinimumSize(300, 1500);
        contentWidget->setLayout(layout);
        setWidget(contentWidget);

        // 垂直スクロールバーのvalueChangedシグナルを接続
        connect(verticalScrollBar(), &QScrollBar::valueChanged, this, &CustomScrollAreaWithScrollBarSignals::onVerticalScrollValueChanged);
        // 水平スクロールバーのvalueChangedシグナルを接続
        connect(horizontalScrollBar(), &QScrollBar::valueChanged, this, &CustomScrollAreaWithScrollBarSignals::onHorizontalScrollValueChanged);
    }

private slots:
    void onVerticalScrollValueChanged(int value)
    {
        qDebug() << "Vertical Scroll Bar Value Changed:" << value;
        // ここで、スクロールバーの新しい値に基づいて、コンテンツの描画オフセットなどを調整するロジックを記述します。
        // QScrollArea の場合は、デフォルトでこれが処理されるので、ここではデバッグ出力のみ。
        // カスタム描画を行う QAbstractScrollArea の派生クラスであれば、ここで viewport()->update() など。
    }

    void onHorizontalScrollValueChanged(int value)
    {
        qDebug() << "Horizontal Scroll Bar Value Changed:" << value;
        // 同上
    }

protected:
    // wheelEvent をオーバーライドしない場合でも、QScrollArea は自動でスクロールバーを動かす
    // もし、wheelEvent で特別な処理を行い、その結果としてスクロールバーの値を変更したい場合は、
    // ここで直接 setValue() を呼び出すことができます。
    // void wheelEvent(QWheelEvent *event) override {
    //     // 例えば、Ctrlキーを押しながらホイールを回すと垂直スクロールが通常の2倍になる
    //     if (event->modifiers() & Qt::ControlModifier) {
    //         verticalScrollBar()->setValue(verticalScrollBar()->value() - event->angleDelta().y() * 2);
    //         event->accept();
    //     } else {
    //         QScrollArea::wheelEvent(event); // デフォルト動作
    //     }
    // }
};

// main.cpp
// #include <QApplication>
// #include <QMainWindow>
// #include "customscrollareawithscrollbar.h"
//
// int main(int argc, char *argv[])
// {
//     QApplication a(argc, argv);
//     QMainWindow window;
//     CustomScrollAreaWithScrollBarSignals *scrollArea = new CustomScrollAreaWithScrollBarSignals(&window);
//     window.setCentralWidget(scrollArea);
//     window.setWindowTitle("Scroll Area with Scroll Bar Signals");
//     window.resize(400, 300);
//     window.show();
//     return a.exec();
// }
  • QScrollBar シグナル/スロットの直接操作

    • ホイールイベント自体ではなく、スクロールバーの状態変化に直接反応して何かをしたい場合に適しています。
    • 例えば、スクロール位置に応じてUI要素を動的に更新したい場合などです。ホイールイベントが間接的にスクロールバーの値を変更するため、この方法でもホイールによる動作に反応できますが、イベント自体をインターセプトするわけではありません。
  • QScroller

    • タッチ操作やマウスドラッグによる慣性スクロール機能を提供したい場合に適しています。
    • 特にモバイルアプリケーションや、タッチパッドに最適化されたデスクトップアプリケーションで、より滑らかなスクロール体験を実現したい場合に考慮されます。
  • イベントフィルター

    • 既存のウィジェット(自分でサブクラス化できない、またはしたくないウィジェット)のホイールイベント動作を変更したい場合に非常に有効です。
    • 複数の異なるウィジェットに対して同じホイールイベント処理を適用したい場合に、再利用可能なコードとして実装できます。
    • イベントの伝播を細かく制御したい場合(例: 特定の条件でイベントを消費し、それ以外では親に渡す)。
  • wheelEvent() オーバーライド

    • 最も直接的で、QAbstractScrollAreaサブクラス自身がホイールイベントを完全に制御したい場合に適しています。
    • カスタムのズームやスクロールロジックをウィジェットに直接組み込みたい場合に最適です。