【Qt入門】QAbstractScrollAreaのmouseMoveEvent徹底解説!マウス操作でUIを動かす

2025-05-26

QAbstractScrollArea は、スクロール可能な領域を提供するQtウィジェットの基底クラスです。このクラスは、その名前が示す通り、スクロールバーを持つエリアの抽象的な機能を提供します。mouseMoveEvent は、この QAbstractScrollArea またはその派生クラス(例: QScrollArea, QTextEdit, QAbstractItemView など)のビューポート(表示領域)上でマウスが移動したときに発生するイベントを処理するための仮想関数です。

どのような時に呼び出されるか

この関数は、以下の条件を満たしたときに自動的に呼び出されます。

  1. マウスがビューポート内を移動したとき: ウィジェットの表示領域内でマウスカーソルが動くと、このイベントが発生します。
  2. マウスボタンが押されている間も: マウスボタンを押しながらドラッグしている場合でも、マウスの移動に応じてこのイベントが連続して発生します。

引数 QMouseEvent *event について

QMouseEvent *event は、マウスイベントに関する詳細な情報を提供するオブジェクトへのポインタです。このオブジェクトから、以下の情報を取得できます。

  • event->modifiers(): イベント発生時に押されていた修飾キー(Shift, Ctrl, Altなど)を示すビットマスクを取得します。
  • event->button(): イベントの原因となった単一のマウスボタンを取得します(クリックイベントの場合)。mouseMoveEvent では、通常 buttons() を使用する方がより有用です。
  • event->buttons(): イベント発生時にどのマウスボタンが押されているかを示すビットマスクを取得します。例えば、Qt::LeftButtonQt::RightButton などとAND演算を行うことで、特定のボタンが押されているかを確認できます。
  • event->globalPos(): イベントが発生した時点でのマウスカーソルのスクリーン座標(画面の左上を原点とする座標)を取得します。
  • event->pos(): イベントが発生した時点でのマウスカーソルのローカル座標(ウィジェットの左上を原点とする座標)を取得します。

使用例とオーバーライド

通常、QAbstractScrollArea はスクロール機能を提供するため、デフォルトの mouseMoveEvent の実装は、スクロールバーの動作に関連する内部的な処理を行っています。しかし、開発者がスクロールエリアのビューポート内で独自のインタラクション(例えば、カスタムなドラッグ&ドロップ、描画、ズームなど)を実装したい場合、この mouseMoveEvent 関数をオーバーライド(再実装)します。

オーバーライドの基本的な構造

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

class MyCustomScrollArea : public QAbstractScrollArea
{
public:
    MyCustomScrollArea(QWidget *parent = nullptr) : QAbstractScrollArea(parent) {
        // マウスのトラッキングを有効にする
        // これがないと、ボタンが押されていない状態ではmouseMoveEventは発生しない
        setMouseTracking(true);
    }

protected:
    void mouseMoveEvent(QMouseEvent *event) override {
        // マウスの現在位置を取得
        QPoint currentPos = event->pos();

        // 例: マウスの位置をデバッグ出力
        qDebug() << "Mouse moved to:" << currentPos;

        // 例: 左ボタンが押されている場合のみ特別な処理を行う
        if (event->buttons() & Qt::LeftButton) {
            // ここにドラッグ中の処理などを記述
            qDebug() << "Left button is pressed. Dragging from:" << lastMousePos << "to:" << currentPos;
            // 必要に応じてビューポートを更新
            // viewport()->update();
        }

        // 必要に応じて、基底クラスのmouseMoveEventを呼び出す
        // これにより、デフォルトのスクロール動作などが維持される
        QAbstractScrollArea::mouseMoveEvent(event);

        // 次のイベントのために現在のマウス位置を保存しておくこともよくある
        lastMousePos = currentPos;
    }

private:
    QPoint lastMousePos; // 前回のマウス位置を保存するためのメンバー変数
};
  • QMouseEvent のデータ: QMouseEvent オブジェクトから得られる情報は、カスタムな描画やインタラクションのロジックを実装する上で非常に重要です。
  • 基底クラスの呼び出し: 独自の処理を追加した後でも、多くの場合 QAbstractScrollArea::mouseMoveEvent(event); を呼び出すことが推奨されます。これは、基底クラスが提供する標準的な動作(例えば、スクロールバーの自動更新など)を維持するためです。もし、独自の処理で完全にデフォルトの動作を置き換えたい場合は、この呼び出しを省略できます。
  • setMouseTracking(true): QAbstractScrollArea (および QWidget)のデフォルトでは、マウスボタンが押されていない状態では mouseMoveEvent は発生しません。ボタンが押されていない状態でもマウス移動イベントを受け取りたい場合は、コンストラクタなどで setMouseTracking(true) を呼び出す必要があります。


QAbstractScrollArea::mouseMoveEvent() は、マウスの移動を処理するための重要なイベントハンドラですが、その使用にはいくつか注意点があります。

マウス移動イベントが発生しない (Button Pressed状態でのみ発生する)

これは最もよくある問題です。

  • 原因: QWidget および QAbstractScrollArea のデフォルトの動作では、マウスボタンが押されていない状態でのマウス移動イベントは発生しません。これはパフォーマンス最適化のためです。

  • エラー/現象:

    • マウスボタンを押しながらドラッグしている間は mouseMoveEvent が呼び出されるが、マウスボタンが押されていない状態でマウスを動かしても何も起こらない。
    • つまり、マウスカーソルがウィジェット上を移動しているだけでイベントを検知したいのに、それができない。

event->button() の誤用 (event->buttons() との混同)

  • トラブルシューティング:

    • event->buttons() を使用してビットマスクをチェックする: どのマウスボタンが現在押されているかを判断するには、event->buttons() が返すビットマスクに対してビット論理積 (&) 演算を行います。

      void MyCustomScrollArea::mouseMoveEvent(QMouseEvent *event) {
          if (event->buttons() & Qt::LeftButton) {
              // 左ボタンが押されながらマウスが移動している
              // ドラッグ中の処理
          }
          if (event->buttons() & Qt::RightButton) {
              // 右ボタンが押されながらマウスが移動している
              // 別の処理
          }
          // ...
          QAbstractScrollArea::mouseMoveEvent(event);
      }
      
  • 原因:

    • QMouseEvent::button() は、イベントを引き起こした単一のマウスボタンを返します。これは主に mousePressEventmouseReleaseEvent で使われます。
    • mouseMoveEvent の場合、マウスを移動している最中に複数のボタンが押されている可能性があり、また、マウス移動イベント自体が「ボタンが押された」という単一のトリガーを持つわけではないため、button() は通常 Qt::NoButton を返します(例外的に、ボタンを押した状態でドラッグを開始した場合、最初の移動イベントではボタン情報が含まれることもありますが、これは信頼できません)。
    • 現在押されているすべてのマウスボタンの状態を取得するには、QMouseEvent::buttons() を使用する必要があります。これはビットマスクで、複数のボタンの状態を同時に表現します。
  • エラー/現象:

    • mouseMoveEvent 内で event->button() を使用して、どのボタンが押されているかを判定しようとすると、期待する結果が得られない。
    • 例えば、if (event->button() == Qt::LeftButton) のような条件が常に false になる場合がある。

イベントが親ウィジェットまたは子ウィジェットで消費されてしまう

  • トラブルシューティング:

    • イベントフローの確認: イベントがどこで消費されているかをデバッグ出力などで確認します。
    • イベントフィルタの確認: 親ウィジェットに installEventFilter() でイベントフィルタが設定されていないか確認し、もしある場合は、そのフィルタのロジックが意図しないイベント消費をしていないか確認します。
    • 子ウィジェットのイベントハンドラの確認: QAbstractScrollArea のビューポート内に配置されている子ウィジェットが、マウスイベントを accept() していないか確認します。必要であれば、子ウィジェットで event->ignore() を呼び出すことで、イベントを親に伝播させることができます。
    • 基底クラスの呼び出し: 独自の処理を行う場合でも、基本的には QAbstractScrollArea::mouseMoveEvent(event); を呼び出すことを忘れないでください。これにより、Qtの標準的なイベント処理フローが維持され、予期しない動作を防げます。ただし、イベントを完全に置き換えたい場合はこの限りではありません。
  • 原因:

    • Qtのイベント処理は、イベントがどのウィジェットに送られるか、そしてそのイベントが「受け入れられる (accepted)」か「無視される (ignored)」かによって挙動が変わります。
    • イベントフィルタ: 親ウィジェットにイベントフィルタがインストールされており、そこでマウス移動イベントが消費(event->accept())されている場合、子ウィジェットにはイベントが届きません。
    • 子ウィジェットによる消費: QAbstractScrollArea のビューポート内に子ウィジェットが配置されており、その子ウィジェットがマウス移動イベントを処理(event->accept())している場合、親である QAbstractScrollArea はそのイベントを受け取れません。
    • デフォルトの実装の欠如: オーバーライドした mouseMoveEvent の中で、基底クラスの QAbstractScrollArea::mouseMoveEvent(event); を呼び出していない場合、Qtの内部的なスクロール処理などが正しく行われない可能性があります。
  • エラー/現象:

    • QAbstractScrollArea を継承したカスタムウィジェットで mouseMoveEvent をオーバーライドしたが、期待通りに呼び出されない。
    • しかし、その親ウィジェットや子ウィジェットのイベントハンドラは呼び出されている。

マウス座標のずれ (特にスクロール時)

  • トラブルシューティング:

    • スクロールオフセットを考慮する: horizontalScrollBar()->value()verticalScrollBar()->value() を使用して、スクロールオフセットを取得し、event->pos() に加算(または減算)してコンテンツ座標に変換します。

      void MyCustomScrollArea::mouseMoveEvent(QMouseEvent *event) {
          QPoint viewportPos = event->pos();
          QPoint contentPos = viewportPos + QPoint(horizontalScrollBar()->value(), verticalScrollBar()->value());
      
          qDebug() << "Mouse moved at content position:" << contentPos;
          // このcontentPosを使って描画やデータアクセスを行う
      
          QAbstractScrollArea::mouseMoveEvent(event);
      }
      
    • viewport()->mapToGlobal()mapFromGlobal() などを使用する: 必要に応じて、グローバル座標や他のウィジェットの座標系との間で変換を行うQtの便利な関数を使用します。

  • 原因:

    • event->pos() は、イベントを受け取ったウィジェットの「ローカル座標」を返します。QAbstractScrollArea の場合、これは通常ビューポートの左上を原点とする座標です。
    • しかし、スクロールエリア内の描画やインタラクションを行う際には、スクロール位置を考慮した「コンテンツ座標」または「論理座標」に変換する必要があります。QAbstractScrollAreaviewport() を持ち、その viewport() の座標系でイベントを受け取りますが、スクロールによって表示されるコンテンツの位置は変わります。
  • エラー/現象:

    • event->pos() で取得したマウス座標が、実際に表示されている内容と合わない。
    • 特に、スクロールを行った後に描画処理などを行うと、マウス位置がずれる。

過剰なイベント発生によるパフォーマンス問題

  • トラブルシューティング:

    • 処理の間引き/遅延:
      • タイマーを使用する: QTimer::singleShot() などを使って、一定時間ごとに処理を実行するようにします。例えば、マウス移動イベントが連続して発生しても、最後のイベントから100ms後に一度だけ処理を実行するといったことができます。
      • フラグを使用する: 処理が必要な場合のみフラグを立て、update() を呼び出して paintEvent で実際に描画を行うようにします。
    • 描画の最適化:
      • update() の代わりに viewport()->update(rect) を使用して、再描画が必要な領域のみを更新するようにします。
      • paintEvent 内での描画処理を効率化します。
    • スレッド化: 非常に重い計算を伴う場合は、QThread を使用して別のスレッドで処理を行い、GUIスレッドの応答性を維持します。ただし、QtのGUIはメインスレッドで操作する必要があるため、スレッド間でのデータのやり取りには注意が必要です(シグナル/スロットを使用するなど)。
  • 原因:

    • マウス移動イベントは、マウスが少しでも動けば非常に高頻度で発生します。そのたびに重い処理を実行すると、システムリソースを大量に消費し、応答性が低下します。
  • エラー/現象:

    • mouseMoveEvent 内で重い処理(例えば、複雑な再描画や大量のデータ処理)を行うと、アプリケーションの動作が遅くなる、またはフリーズする。


例1: マウス位置の表示とシンプルな描画(マウスボタン押下時)

この例では、QAbstractScrollArea を継承したカスタムウィジェットを作成し、マウスが移動するたびに現在のマウス位置をデバッグ出力し、さらにマウスの左ボタンが押されている間にドラッグの軌跡を描画します。

mycustomscrollarea.h

#ifndef MYCUSTOMSCROLLAREA_H
#define MYCUSTOMSCROLLAREA_H

#include <QAbstractScrollArea>
#include <QMouseEvent>
#include <QPainter>
#include <QDebug> // デバッグ出力用
#include <QVector> // 軌跡を保存するため

class MyCustomScrollArea : public QAbstractScrollArea
{
    Q_OBJECT

public:
    explicit MyCustomScrollArea(QWidget *parent = nullptr);

protected:
    // マウスイベントハンドラのオーバーライド
    void mousePressEvent(QMouseEvent *event) override;
    void mouseMoveEvent(QMouseEvent *event) override;
    void mouseReleaseEvent(QMouseEvent *event) override;
    void paintEvent(QPaintEvent *event) override;

private:
    QPoint lastMousePos; // 前回のマウス位置
    QVector<QPoint> drawingPoints; // ドラッグ軌跡の座標を保存
    bool isDrawing; // 描画中かどうか
};

#endif // MYCUSTOMSCROLLAREA_H

mycustomscrollarea.cpp

#include "mycustomscrollarea.h"
#include <QScrollBar> // スクロールバーの値を考慮するため

MyCustomScrollArea::MyCustomScrollArea(QWidget *parent)
    : QAbstractScrollArea(parent), isDrawing(false)
{
    // 重要: マウスボタンが押されていない状態でもmouseMoveEventを発生させる
    // setMouseTracking(true); // 今回の例では、ボタン押下時のみ描画するため不要だが、
                             // 必要に応じてコメントアウトを外す
    viewport()->setBackgroundRole(QPalette::Light); // ビューポートの背景色を設定
    viewport()->setAutoFillBackground(true); // 背景を自動で塗りつぶす
}

void MyCustomScrollArea::mousePressEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton) {
        isDrawing = true;
        // マウスプレス時の座標を保存(スクロール位置を考慮)
        lastMousePos = event->pos() + QPoint(horizontalScrollBar()->value(), verticalScrollBar()->value());
        drawingPoints.clear(); // 新しい描画を開始するので、既存の点をクリア
        drawingPoints.append(lastMousePos); // 最初の点を追加
        qDebug() << "Mouse Pressed at (Content):" << lastMousePos;
    }
    // 基底クラスのイベントハンドラを呼び出す (通常は不要だが、特殊なケースで影響を与える可能性も)
    QAbstractScrollArea::mousePressEvent(event);
}

void MyCustomScrollArea::mouseMoveEvent(QMouseEvent *event)
{
    // 現在のマウス位置をデバッグ出力 (ビューポート座標)
    qDebug() << "Mouse Move (Viewport):" << event->pos();

    // 左ボタンが押されている場合のみ描画処理を行う
    if (isDrawing && (event->buttons() & Qt::LeftButton)) {
        // 現在のマウス位置をコンテンツ座標に変換
        QPoint currentContentPos = event->pos() + QPoint(horizontalScrollBar()->value(), verticalScrollBar()->value());

        // 描画点と前回の点との距離が一定以上ある場合のみ追加(描画を滑らかにするため)
        if ((currentContentPos - drawingPoints.last()).manhattanLength() > 2) {
            drawingPoints.append(currentContentPos);
            // 描画領域を更新 (描画範囲に合わせて指定するのが理想だが、ここでは全体を更新)
            viewport()->update();
        }
        lastMousePos = currentContentPos; // 次のイベントのために現在の位置を保存
    }

    // 基底クラスのイベントハンドラを呼び出す (デフォルトのスクロール動作などを維持するため)
    QAbstractScrollArea::mouseMoveEvent(event);
}

void MyCustomScrollArea::mouseReleaseEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton) {
        isDrawing = false;
        qDebug() << "Mouse Released";
    }
    QAbstractScrollArea::mouseReleaseEvent(event);
}

void MyCustomScrollArea::paintEvent(QPaintEvent *event)
{
    // 基底クラスのpaintEventを呼び出す (スクロールエリアの背景などが描画される)
    QAbstractScrollArea::paintEvent(event);

    QPainter painter(viewport()); // ビューポートに描画
    painter.setPen(QPen(Qt::blue, 2, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));

    // スクロールオフセットを考慮して座標を変換する (重要!)
    // painter.translate(-horizontalScrollBar()->value(), -verticalScrollBar()->value());
    // もしくは、QPainter::setWindow()/setViewport() を使う方法もある

    // drawingPointsはコンテンツ座標で保存されているので、
    // ここでpainterをスクロールオフセット分だけ逆方向に移動させるか、
    // 各点を描画時に変換する必要がある。
    // 例: 各点を描画時に変換する方法
    QPoint offset(-horizontalScrollBar()->value(), -verticalScrollBar()->value());

    if (drawingPoints.size() > 1) {
        for (int i = 0; i < drawingPoints.size() - 1; ++i) {
            painter.drawLine(drawingPoints[i] + offset, drawingPoints[i+1] + offset);
        }
    }
}

main.cpp

#include <QApplication>
#include <QMainWindow>
#include "mycustomscrollarea.h"

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

    QMainWindow window;
    MyCustomScrollArea *scrollArea = new MyCustomScrollArea(&window);
    window.setCentralWidget(scrollArea);
    window.setWindowTitle("Custom Scroll Area Drawing Example");
    window.resize(600, 400); // ウィンドウサイズを少し大きく設定
    window.show();

    return a.exec();
}

解説

  • paintEvent:
    • QPainterviewport() 上に作成し、描画を行います。
    • 重要: drawingPoints はコンテンツ座標で保存されています。paintEvent が呼び出される際、QPainter はビューポートの左上を原点とします。そのため、描画を行う際にスクロールオフセットを考慮する必要があります。例では、drawingPoints[i] + offset のように各点にスクロールオフセットを適用して、ビューポート内の正しい位置に描画しています。
    • QAbstractScrollArea::paintEvent(event); を呼び出すことで、基底クラスが提供するデフォルトの描画(背景など)が行われます。
  • mouseReleaseEvent: 左ボタンが離されたときに描画を停止します。
  • mouseMoveEvent:
    • qDebug() を使って、マウスが移動するたびにビューポート座標を表示します。
    • event->buttons() & Qt::LeftButton で、左ボタンが押されているかをチェックします。
    • event->pos() で得られるビューポート座標に、スクロールバーの値を加算することで、コンテンツ座標に変換しています。これにより、スクロールしても正しい位置に描画できます。
    • drawingPoints に変換後のコンテンツ座標を追加し、viewport()->update() を呼び出して再描画を促します。
  • mousePressEvent: 左ボタンが押されたときに描画を開始し、drawingPoints の初期点を設定します。lastMousePos はコンテンツ座標で保存されます。
  • コンストラクタ: setMouseTracking(true) は今回の例ではコメントアウトされています。なぜなら、マウスボタンが押されている間だけ描画を行うため、デフォルトの動作(ボタン押下時のみ mouseMoveEvent が発生)で十分だからです。もし、ボタンが押されていない状態でもマウス移動を検知したい場合は、この行のコメントを外してください。
  • MyCustomScrollArea クラス: QAbstractScrollArea を継承します。

例2: マウス座標に応じたテキスト表示(マウスボタンが押されていなくても)

この例では、setMouseTracking(true) を使用して、マウスボタンが押されていなくても mouseMoveEvent を発生させ、現在のマウス位置をテキストで表示します。

mytrackingarea.h

#ifndef MYTRACKINGAREA_H
#define MYTRACKINGAREA_H

#include <QAbstractScrollArea>
#include <QMouseEvent>
#include <QPainter>
#include <QString>

class MyTrackingArea : public QAbstractScrollArea
{
    Q_OBJECT

public:
    explicit MyTrackingArea(QWidget *parent = nullptr);

protected:
    void mouseMoveEvent(QMouseEvent *event) override;
    void paintEvent(QPaintEvent *event) override;

private:
    QPoint currentMousePos; // 現在のマウス位置 (コンテンツ座標)
    QString statusText;     // 表示するテキスト
};

#endif // MYTRACKINGAREA_H

mytrackingarea.cpp

#include "mytrackingarea.h"
#include <QScrollBar>

MyTrackingArea::MyTrackingArea(QWidget *parent)
    : QAbstractScrollArea(parent)
{
    // 重要: マウスボタンが押されていない状態でもmouseMoveEventを発生させる
    setMouseTracking(true);
    viewport()->setBackgroundRole(QPalette::Light);
    viewport()->setAutoFillBackground(true);
    statusText = "Move mouse over here...";
}

void MyTrackingArea::mouseMoveEvent(QMouseEvent *event)
{
    // 現在のマウス位置をコンテンツ座標に変換して保存
    currentMousePos = event->pos() + QPoint(horizontalScrollBar()->value(), verticalScrollBar()->value());

    // 表示するテキストを更新
    statusText = QString("Mouse X: %1, Y: %2 (Content)").arg(currentMousePos.x()).arg(currentMousePos.y());

    // ビューポートを更新してテキストを再描画させる
    viewport()->update();

    // 基底クラスのイベントハンドラを呼び出す
    QAbstractScrollArea::mouseMoveEvent(event);
}

void MyTrackingArea::paintEvent(QPaintEvent *event)
{
    QAbstractScrollArea::paintEvent(event);

    QPainter painter(viewport());
    painter.setPen(Qt::black);
    painter.setFont(QFont("Arial", 12));

    // テキストを表示する位置 (ビューポート座標)
    // スクロールしても常に同じ位置に表示したいので、スクロールオフセットは加算しない
    painter.drawText(10, 20, statusText);

    // デバッグ用の十字線を描画 (コンテンツ座標をビューポート座標に戻して描画)
    QPoint drawPos = currentMousePos - QPoint(horizontalScrollBar()->value(), verticalScrollBar()->value());
    painter.setPen(Qt::red);
    painter.drawLine(drawPos.x() - 10, drawPos.y(), drawPos.x() + 10, drawPos.y());
    painter.drawLine(drawPos.x(), drawPos.y() - 10, drawPos.x(), drawPos.y() + 10);
}

main.cpp (例1と同じでOK)

解説

  • paintEvent:
    • statusText を描画します。テキストは常にビューポートの左上から少しずらした位置に表示されるため、スクロールオフセットは加算しません。
    • デバッグ用に、currentMousePos (コンテンツ座標) をビューポート座標に変換し直して十字線を描画しています。これにより、スクロールしても十字線がマウスカーソルに追従することを確認できます。
  • mouseMoveEvent:
    • event->pos() を使って現在のマウス位置(ビューポート座標)を取得し、これにスクロールオフセットを加えてコンテンツ座標 currentMousePos を計算します。
    • この currentMousePos を使って表示するテキスト statusText を更新します。
    • viewport()->update() を呼び出して、テキストが更新されたことをビューポートに知らせ、再描画を促します。
  • コンストラクタ: setMouseTracking(true) を呼び出すことで、マウスボタンが押されていなくても mouseMoveEvent が発生するようにします。

これらの例は、QAbstractScrollArea::mouseMoveEvent() をオーバーライドして、マウスの移動イベントを処理する方法を示しています。重要なポイントは以下の通りです。

  • 再描画: 描画内容を変更した場合は、viewport()->update() を呼び出して再描画を促すことを忘れないでください。
  • 基底クラスの呼び出し: 通常は QAbstractScrollArea::mouseMoveEvent(event); を呼び出して、基底クラスのデフォルト動作(スクロールなど)を維持するようにしてください。完全に動作を置き換えたい場合は省略できます。
  • 座標の変換: QAbstractScrollArea の場合、event->pos() はビューポートのローカル座標です。スクロールしたコンテンツ上で作業する場合は、horizontalScrollBar()->value()verticalScrollBar()->value() を使ってコンテンツ座標に変換する必要があります。
  • event->buttons() vs event->button(): マウスの移動イベントでボタンの状態をチェックする場合は、常に event->buttons() を使用し、ビットマスクをチェックしてください。
  • setMouseTracking(true): マウスボタンが押されていない状態でもイベントを検知したい場合に必要です。


QAbstractScrollArea::mouseMoveEvent() の代替方法

イベントフィルタ (QObject::installEventFilter)

これは、特定のウィジェットに直接サブクラス化してイベントをオーバーライドするのではなく、別のオブジェクトがそのウィジェットのイベントを監視・処理できるようにする強力なメカニズムです。

  • 使用例: QAbstractScrollArea のビューポート(viewport())にイベントフィルタをインストールして、マウス移動イベントを捕捉します。

    // myeventfilter.h
    #include <QObject>
    #include <QEvent>
    #include <QMouseEvent>
    #include <QDebug>
    #include <QAbstractScrollArea> // スクロールバーの値を考慮するため
    
    class MyEventFilter : public QObject
    {
        Q_OBJECT
    public:
        explicit MyEventFilter(QObject *parent = nullptr) : QObject(parent) {}
    
    protected:
        bool eventFilter(QObject *watched, QEvent *event) override {
            if (event->type() == QEvent::MouseMove) {
                QMouseEvent *mouseEvent = static_cast<QMouseEvent*>(event);
                QAbstractScrollArea *scrollArea = qobject_cast<QAbstractScrollArea*>(watched->parent()); // ビューポートの親はQAbstractScrollArea
    
                if (scrollArea) {
                    QPoint viewportPos = mouseEvent->pos();
                    QPoint contentPos = viewportPos + QPoint(scrollArea->horizontalScrollBar()->value(), scrollArea->verticalScrollBar()->value());
                    qDebug() << "Filtered Mouse Move (Content):" << contentPos;
    
                    // ここでカスタムロジックを実行
    
                    // イベントをそのまま続行させるか、ここで消費するか選択
                    // return true;  // イベントを消費し、ターゲットオブジェクトには渡さない
                    // return false; // イベントをターゲットオブジェクトに渡す
                }
            }
            return QObject::eventFilter(watched, event); // 他のイベントは基底クラスに任せる
        }
    };
    
    // main.cpp または MyScrollAreaのコンストラクタ内で
    // MyCustomScrollArea *scrollArea = new MyCustomScrollArea(...);
    // MyEventFilter *filter = new MyEventFilter(scrollArea); // 親を設定してメモリ管理を任せる
    // scrollArea->viewport()->installEventFilter(filter); // ビューポートにフィルタをインストール
    // scrollArea->viewport()->setMouseTracking(true); // フィルタでボタン無し移動も検知する場合
    
  • 欠点:

    • イベント処理のロジックがウィジェット自身から分離されるため、コードの可読性が低下する場合があります。
    • フィルタでイベントを accept() すると、元のウィジェットのイベントハンドラが呼び出されなくなります。
  • 利点:

    • 既存のクラスをサブクラス化する必要がないため、Qtの既存のウィジェットの動作を変更することなく、イベント処理を追加できます。
    • 単一のイベントフィルタオブジェクトが複数のウィジェットのイベントを処理できます。
    • イベントがターゲットウィジェットに到達する前に処理(または破棄)できます。

シグナルとスロット (QGraphicsView の場合など)

QGraphicsView のように、QAbstractScrollArea を継承しつつ、さらに高度なグラフィックスシーンを提供するウィジェットの場合、マウスイベントを直接オーバーライドするのではなく、シーンまたはアイテムからのシグナルを利用する方が自然な場合があります。

  • 使用例 (QGraphicsView の場合):

    // QGraphicsViewを継承
    class MyGraphicsView : public QGraphicsView
    {
        Q_OBJECT
    public:
        explicit MyGraphicsView(QGraphicsScene *scene, QWidget *parent = nullptr)
            : QGraphicsView(scene, parent)
        {
            setMouseTracking(true); // マウス追跡を有効にする
        }
    
    protected:
        // mouseMoveEventをオーバーライドすることもできるが、
        // QGraphicsViewのシグナルを使う方がQGraphicsScene/Itemとの連携がスムーズ
        void mouseMoveEvent(QMouseEvent *event) override {
            // ビューポートの座標からシーンの座標に変換
            QPointF scenePos = mapToScene(event->pos());
            qDebug() << "View Mouse Move (Scene):" << scenePos;
            QGraphicsView::mouseMoveEvent(event); // 基底クラス呼び出し
        }
    };
    
    // QGraphicsSceneを使用
    class MyGraphicsScene : public QGraphicsScene
    {
        Q_OBJECT
    public:
        explicit MyGraphicsScene(QObject *parent = nullptr) : QGraphicsScene(parent) {
            // シーンのマウス移動イベントを有効にする (QGraphicsViewでsetMouseTracking(true)が必要)
            // これにより、QGraphicsItem::hoverMoveEventなどが呼び出される
        }
    
    protected:
        // シーンレベルでのマウス移動を捕捉 (特定のアイテムに限定されない場合)
        void mouseMoveEvent(QGraphicsSceneMouseEvent *mouseEvent) override {
            qDebug() << "Scene Mouse Move:" << mouseEvent->scenePos();
            QGraphicsScene::mouseMoveEvent(mouseEvent);
        }
    };
    
    // QGraphicsItemを継承 (アイテム上でのマウス移動)
    class MyGraphicsItem : public QGraphicsRectItem
    {
    public:
        MyGraphicsItem(const QRectF &rect) : QGraphicsRectItem(rect) {
            setAcceptHoverEvents(true); // ホバーイベントを受け入れる
        }
    protected:
        void hoverMoveEvent(QGraphicsSceneHoverEvent *event) override {
            qDebug() << "Item Hover Move:" << event->pos(); // アイテムローカル座標
            QGraphicsRectItem::hoverMoveEvent(event);
        }
    };
    
  • 欠点:

    • QGraphicsView に限定されるアプローチです。
    • 単純な QAbstractScrollArea では直接利用できません。
  • 利点:

    • Qtのシグナル&スロットメカニズムに統合されており、疎結合な設計が可能です。
    • QGraphicsSceneQGraphicsItem が豊富なマウスイベントシグナルを提供します。

イベントの再実装 (QWidget::event())

QWidget::event() は、Qtのイベント処理メカニズムの最上位の仮想関数です。すべてのイベント(マウス、キーボード、ペイントなど)は、最終的にこの関数を通過します。

  • 使用例:

    #include <QAbstractScrollArea>
    #include <QMouseEvent>
    #include <QDebug>
    
    class MyCustomScrollArea : public QAbstractScrollArea
    {
        Q_OBJECT
    
    public:
        explicit MyCustomScrollArea(QWidget *parent = nullptr) : QAbstractScrollArea(parent) {
            setMouseTracking(true);
        }
    
    protected:
        bool event(QEvent *event) override {
            if (event->type() == QEvent::MouseMove) {
                QMouseEvent *mouseEvent = static_cast<QMouseEvent*>(event);
                qDebug() << "Event() Mouse Move (Viewport):" << mouseEvent->pos();
                // ここでカスタムロジックを実行
    
                // 必要に応じて、ここでイベントを消費する
                // mouseEvent->accept();
                // return true; // イベントが処理されたことを示す
    
                // イベントをmouseMoveEvent()ハンドラに渡す(基底クラスのevent()がそれを行う)
                return QAbstractScrollArea::event(event);
            }
            // 他のイベントは基底クラスのevent()に任せる
            return QAbstractScrollArea::event(event);
        }
    
        // 通常のmouseMoveEvent()も引き続き呼び出される(event()で消費しない限り)
        void mouseMoveEvent(QMouseEvent *event) override {
            qDebug() << "mouseMoveEvent() called.";
            QAbstractScrollArea::mouseMoveEvent(event);
        }
    };
    
  • 欠点:

    • 通常は特定のイベントハンドラ(mouseMoveEvent など)をオーバーライドする方が、コードが読みやすく、保守しやすくなります。
    • event() をオーバーライドする場合、自分でイベントタイプをチェックし、適切なハンドラを呼び出す必要があります(または基底クラスに任せる)。
  • 利点:

    • すべてのイベントタイプを単一の場所で処理できるため、イベントディスパッチロジックを一元化できます。
    • イベントハンドラ(mouseMoveEvent など)が呼び出される前にイベントを傍受できます。

QCursor と QApplication::overrideCursor()

これはイベント処理とは異なりますが、マウスカーソルの形状を変更して、特定の操作(例: ドラッグ)が進行中であることをユーザーに視覚的にフィードバックするために使われる一般的な手法です。

  • 使用例:

    void MyCustomScrollArea::mousePressEvent(QMouseEvent *event)
    {
        if (event->button() == Qt::LeftButton) {
            // ドラッグ開始時にカーソルをHandCursorに変更
            QApplication::overrideCursor(Qt::OpenHandCursor);
        }
        QAbstractScrollArea::mousePressEvent(event);
    }
    
    void MyCustomScrollArea::mouseReleaseEvent(QMouseEvent *event)
    {
        if (event->button() == Qt::LeftButton) {
            // ドラッグ終了時にカーソルを元に戻す
            QApplication::restoreOverrideCursor();
        }
        QAbstractScrollArea::mouseReleaseEvent(event);
    }
    
    // mouseMoveEvent内で、必要に応じてカーソルの変更を維持
    void MyCustomScrollArea::mouseMoveEvent(QMouseEvent *event)
    {
        // ... (通常のマウス移動処理) ...
    
        // 例えば、特定の領域に入ったらカーソルを変更する
        // if (someCondition) {
        //     viewport()->setCursor(Qt::PointingHandCursor);
        // } else {
        //     viewport()->unsetCursor(); // 元に戻す
        // }
        QAbstractScrollArea::mouseMoveEvent(event);
    }
    
  • 欠点:

    • イベントを処理するわけではありません。
    • マウスイベントハンドラと組み合わせて使用する必要があります。
  • 利点:

    • ユーザーエクスペリエンスを向上させます。
    • 非常にシンプルにカーソル形状を変更できます。

選択肢は、要件と設計によって異なります。

  • QCursor: UIの視覚的なフィードバックを改善したい場合に、上記イベント処理方法と組み合わせて使用します。
  • event() の再実装: イベントの低レベルなディスパッチプロセスを完全に制御したい場合、または複数のイベントタイプを単一の場所で非常に特殊な方法で処理したい場合に限定的に使用します。ほとんどの場合、特定の mouseMoveEvent() などのハンドラをオーバーライドする方が良いです。
  • シグナルとスロット (QGraphicsView): グラフィックスシーンとアイテムベースのインタラクションを構築している場合に最適です。Qtのグラフィックスビューフレームワークの強みを活かせます。
  • イベントフィルタ: 既存のウィジェットの振る舞いを変更せずにイベントを監視したい場合、または、単一のオブジェクトで複数のウィジェットのイベントを処理したい場合に適しています。デバッグや特定のウィジェットのイベントを一時的に傍受するのにも便利です。