初心者向けQt Graphics View: contextMenuEventでリッチな右クリックメニューを実装

2025-05-27

役割

QGraphicsViewQGraphicsScene の内容を表示するためのウィジェットです。ユーザーが QGraphicsView の上で右クリック(またはコンテキストメニューをトリガーする操作)を行った際に、この contextMenuEvent が発生します。

このイベントハンドラの主な役割は、以下の通りです。

  1. ビューのコンテキストメニュー処理: QGraphicsView 自体に対してコンテキストメニューを表示したい場合に、この関数を再実装(オーバーライド)します。
  2. イベントの伝播: デフォルトの実装では、このイベントはビューが管理する QGraphicsScene に転送され、さらにシーン上の QGraphicsItem に伝播されます。これにより、シーン上の特定のアイテムに対して個別のコンテキストメニューを表示することができます。

動作の仕組み

  1. 右クリックの検出: ユーザーが QGraphicsView のどこかを右クリックすると、QGraphicsViewQContextMenuEvent を受け取ります。
  2. contextMenuEvent の呼び出し: QGraphicsView は、このイベントを受け取ると、自身の contextMenuEvent(QContextMenuEvent *event) メソッドを呼び出します。
  3. デフォルトの挙動:
    • QGraphicsView のデフォルトの contextMenuEvent 実装は、このイベントを QGraphicsScene に転送します。
    • QGraphicsScene は、イベント発生位置に QGraphicsItem があるかどうかを調べます。
    • アイテムが存在する場合、そのアイテムの contextMenuEvent(QGraphicsSceneContextMenuEvent *event) が呼び出されます。
    • もし、どのアイテムもイベントを処理しなかった場合(event->ignore() を呼び出した場合)、またはイベント発生位置にアイテムがなかった場合、シーンはイベントを無視し、イベントは最終的にビューに戻ってきます。
    • ビューは、そのイベントがまだ処理されていない場合、自身のコンテキストメニューを表示する機会を得ます。
  4. 再実装(オーバーライド):
    • QGraphicsView のサブクラスを作成し、contextMenuEvent() をオーバーライドすることで、ビュー固有のコンテキストメニューロジックを実装できます。
    • 通常、オーバーライドしたメソッド内で QMenu オブジェクトを作成し、QAction を追加して、menu->exec(event->globalPos()) などを使ってメニューを表示します。
    • 重要なのは、イベントを処理したかどうかevent->accept() または event->ignore() で明示することです。
      • event->accept(): イベントが処理されたことを示し、それ以上の伝播を停止します。
      • event->ignore(): イベントが処理されなかったことを示し、イベントは親ウィジェットやデフォルトのハンドラに伝播されます。

使用例(基本的な考え方)

QGraphicsView のサブクラスで contextMenuEvent を再実装する例を以下に示します。

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

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

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

protected:
    // contextMenuEvent をオーバーライド
    void contextMenuEvent(QContextMenuEvent *event) override
    {
        // まず、イベント発生位置にアイテムがあるかを確認する
        QGraphicsItem *item = itemAt(event->pos());

        if (item) {
            // アイテムが存在する場合、アイテムのコンテキストメニュー処理に任せる
            // (デフォルトの QGraphicsView の実装が QGraphicsScene に転送し、
            //  そこからアイテムに転送されるため、通常はここで特別な処理は不要だが、
            //  必要に応じて item->contextMenuEvent() を明示的に呼び出すことも可能)
            qDebug() << "Item found at context menu position.";
            QGraphicsView::contextMenuEvent(event); // デフォルトのイベント処理を呼び出す
        } else {
            // アイテムが存在しない場合、ビュー独自のコンテキストメニューを表示
            qDebug() << "No item at context menu position. Showing view context menu.";
            QMenu menu(this);
            QAction *action1 = menu.addAction("ビューのアクション1");
            QAction *action2 = menu.addAction("ビューのアクション2");

            // アクションがクリックされた際のシグナル/スロット接続
            connect(action1, &QAction::triggered, [](){ qDebug() << "ビューのアクション1が選択されました"; });
            connect(action2, &QAction::triggered, [](){ qDebug() << "ビューのアクション2が選択されました"; });

            // メニューを表示
            // globalPos() はスクリーン座標におけるマウスの位置を返します
            menu.exec(event->globalPos());

            // イベントを処理したことを示す
            event->accept();
        }
    }
};

この例では、右クリックされた位置にQGraphicsItemがあるかどうかで、挙動を変えています。

  • アイテムがない場合は、QGraphicsView独自のコンテキストメニューを表示しています。
  • アイテムがある場合は、デフォルトのQGraphicsView::contextMenuEvent(event)を呼び出すことで、アイテムのコンテキストメニューが優先的に表示されるようにしています。

QWidgetQGraphicsViewも継承しています)には contextMenuPolicy というプロパティがあります。これは、コンテキストメニューイベントをどのように処理するかを制御します。

  • Qt::CustomContextMenu: contextMenuEvent() は呼び出されず、代わりに customContextMenuRequested(const QPoint &pos) シグナルが発せられます。このシグナルを接続して、カスタムのコンテキストメニューロジックを実装することもできます。
  • Qt::DefaultContextMenu (デフォルト): contextMenuEvent() ハンドラが呼び出されます。上記で説明したような、イベントの伝播が行われます。

多くの場合、QGraphicsViewcontextMenuEventをオーバーライドして、イベントの伝播とビュー独自のメニューを両方処理する方法が一般的です。しかし、よりシンプルなカスタムメニューが必要な場合は、Qt::CustomContextMenuポリシーとシグナルを使用することもできます。



QGraphicsView のコンテキストメニューイベントは、QGraphicsSceneQGraphicsItem との連携が複雑になるため、いくつかの一般的な落とし穴があります。

ビューのコンテキストメニューが表示されない、またはアイテムのメニューが表示されない

原因

  • アイテムの contextMenuEvent で event->ignore() を呼び出している
    QGraphicsItemcontextMenuEventevent->ignore() を呼び出すと、イベントはシーンに、さらにビューに伝播されます。意図せず ignore() している場合、メニューが表示されないことがあります。
  • デフォルトの実装の呼び出し忘れ
    QGraphicsView::contextMenuEvent(event) をオーバーライドした際に、親クラスのメソッドを呼び出し忘れている。これにより、イベントが QGraphicsSceneQGraphicsItem に適切に伝播されません。
  • イベントの伝播の停止
    contextMenuEvent() をオーバーライドした際に、event->accept() を呼び出してイベントの伝播を早期に停止してしまっている。これにより、QGraphicsSceneQGraphicsItem にイベントが届かなくなります。
  • Qt::ContextMenuPolicy の設定ミス
    QGraphicsViewcontextMenuPolicyQt::CustomContextMenu に設定されており、customContextMenuRequested シグナルにスロットが接続されていない、またはそのスロット内でコンテキストメニューが表示されていない。この場合、contextMenuEvent() は呼び出されません。

トラブルシューティング

  • イベントの伝播の確認
    • QGraphicsView::contextMenuEvent() をオーバーライドする際、アイテムがある場合は QGraphicsView::contextMenuEvent(event); を呼び出すようにしてください。これにより、イベントがシーンとアイテムに適切に伝播されます。
    • アイテムがない場合にビューのメニューを表示し、その際に event->accept() を呼び出すようにします。
    • QGraphicsItemcontextMenuEvent() 内で、意図しない event->ignore() がないか確認してください。
  • contextMenuPolicy の確認
    • QGraphicsViewsetContextMenuPolicy(Qt::DefaultContextMenu) を明示的に設定してみてください。これにより、contextMenuEvent() が確実に呼び出されます。
    • Qt::CustomContextMenu を使用する場合は、必ず customContextMenuRequested シグナルを適切なスロットに接続し、そのスロット内でメニューを表示するロジックを実装してください。

常にビューのコンテキストメニューが表示される

原因

  • QGraphicsView::contextMenuEvent() の実装でアイテムチェックをしていない
    QGraphicsView のオーバーライドされた contextMenuEvent() 内で、右クリックされた位置にアイテムがあるかどうかをチェックせずに、常にビューのメニューを表示するロジックになっている。
  • アイテムの contextMenuEvent が処理されていない
    QGraphicsItemcontextMenuEvent() が適切に実装されていないか、event->accept() が呼び出されていないため、常に event->ignore() 状態となり、イベントがビューにまで伝播してしまう。

トラブルシューティング

  • QGraphicsView::contextMenuEvent() の実装の修正
    • 右クリックされた位置に QGraphicsItem が存在するかどうかを itemAt(event->pos()) でチェックし、存在する場合は親クラスの QGraphicsView::contextMenuEvent(event) を呼び出してイベントをアイテムに転送するようにします。
    • アイテムが存在しない場合にのみ、ビュー独自のコンテキストメニューを表示するロジックを記述します。
    • 前述の例のコードを参照してください。
  • アイテムの contextMenuEvent の確認
    • QGraphicsItem のサブクラスで contextMenuEvent(QGraphicsSceneContextMenuEvent *event) をオーバーライドしていることを確認してください。
    • その中で、メニューを表示したら必ず event->accept() を呼び出すようにしてください。
    • 例:
      void MyGraphicsItem::contextMenuEvent(QGraphicsSceneContextMenuEvent *event)
      {
          QMenu menu;
          menu.addAction("アイテムのアクション");
          // ...
          menu.exec(event->screenPos()); // スクリーン座標で表示
          event->accept(); // イベントを処理したことを示す
      }
      

メニューの表示位置がずれる

原因

  • アイテムのメニュー表示でシーン座標を直接使っている
    QGraphicsItem::contextMenuEvent() では、QGraphicsSceneContextMenuEvent が渡されますが、その中の座標 (event->screenPos(), event->scenePos(), event->pos()) を正しく使わないと位置がずれます。特に QMenu::exec() はスクリーン座標を期待するため、event->screenPos() を使うのが一般的です。
  • 座標系の混同
    QContextMenuEvent はビューポートのローカル座標 (event->pos()) とグローバルスクリーン座標 (event->globalPos()) を持っています。QMenu::exec() は通常、グローバルスクリーン座標を期待しますが、誤ってローカル座標を使用している場合があります。

トラブルシューティング

  • QGraphicsItem::contextMenuEvent() には event->screenPos() を使用
    • QGraphicsItem::contextMenuEvent() 内でアイテムのメニューを表示する場合:
      menu.exec(event->screenPos());
      
  • QMenu::exec() には event->globalPos() を使用
    • QGraphicsView::contextMenuEvent() 内でビューのメニューを表示する場合:
      menu.exec(event->globalPos());
      

特定のアイテムだけメニューが表示されない

原因

  • イベントフィルタの影響
    QGraphicsViewQGraphicsScene、あるいは親ウィジェットにイベントフィルタがインストールされており、コンテキストメニューイベントを横取りしてしまっている。
  • アイテムの contextMenuEvent() がオーバーライドされていない
    そもそも、その特定のアイテムのサブクラスで contextMenuEvent() が実装されていない。
  • アイテムのクリック可能/選択可能設定
    QGraphicsItem::setFlags()ItemIsSelectableItemIsMovable など、イベントを受け取るための適切なフラグが設定されていない。

トラブルシューティング

  • イベントフィルタの確認
    • もしイベントフィルタを使っている場合、そのフィルタがコンテキストメニューイベントを不適切に処理していないかデバッグで追ってみてください。
  • アイテムのオーバーライドを確認
    • そのアイテムのクラス定義で void contextMenuEvent(QGraphicsSceneContextMenuEvent *event) override; が適切に宣言・実装されているか確認します。
  • アイテムのフラグを確認
    • 対象の QGraphicsItem がイベントを受け取れる状態にあるか、setFlag(QGraphicsItem::ItemSendsContextMenuEvents, true) や他の関連するフラグが設定されているか確認します。

コンテキストメニューが表示された後、他のマウスイベントが反応しない

原因

  • イベントのロック
    QMenu::exec() はモーダルなイベントループを開始するため、メニューが閉じられるまで他のマウスイベントが処理されないのが通常の挙動です。これはエラーではなく、期待される動作です。
  • これは通常のエラーではありません。ユーザーがメニューから選択を行うか、メニューの外をクリックしてメニューが閉じられるまで、他のマウスイベントはブロックされます。

デバッグのヒント

  • Qt のドキュメントを参照する
    QGraphicsView, QGraphicsScene, QGraphicsItemcontextMenuEvent に関する公式ドキュメントは非常に詳細で、実装のベストプラクティスが記載されています。
  • イベントオブジェクトの内容を確認する
    QContextMenuEventQGraphicsSceneContextMenuEventpos(), globalPos(), reason() などのプロパティをデバッグ出力して、イベントの状態を把握します。
  • qDebug() を活用する
    contextMenuEvent()QGraphicsItemcontextMenuEvent() の内部に qDebug() を仕込み、イベントがどの経路をたどっているか、どの時点で accept()/ignore() されているかを確認します。


  1. QGraphicsView 自体にコンテキストメニューを表示する
  2. QGraphicsItem にコンテキストメニューを表示する (QGraphicsView からの伝播を利用)
  3. ビューとアイテムの両方でコンテキストメニューを切り替える (一般的な実装パターン)

QGraphicsView 自体にコンテキストメニューを表示する

これは、QGraphicsView の背景(つまり、どの QGraphicsItem も存在しない領域)を右クリックしたときにメニューを表示するケースです。最もシンプルな例です。

mygraphicsview.h

#ifndef MYGRAPHICSVIEW_H
#define MYGRAPHICSVIEW_H

#include <QGraphicsView>
#include <QContextMenuEvent>
#include <QMenu> // QMenu を使用するために必要
#include <QAction> // QAction を使用するために必要
#include <QDebug> // デバッグ出力用

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

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

protected:
    // contextMenuEvent をオーバーライドします
    void contextMenuEvent(QContextMenuEvent *event) override;
};

#endif // MYGRAPHICSVIEW_H

mygraphicsview.cpp

#include "mygraphicsview.h"

MyGraphicsView::MyGraphicsView(QWidget *parent)
    : QGraphicsView(parent)
{
    // コンテキストメニューポリシーをデフォルトに設定します
    // これにより contextMenuEvent() が呼び出されます
    setContextMenuPolicy(Qt::DefaultContextMenu);
}

MyGraphicsView::MyGraphicsView(QGraphicsScene *scene, QWidget *parent)
    : QGraphicsView(scene, parent)
{
    setContextMenuPolicy(Qt::DefaultContextMenu);
}

void MyGraphicsView::contextMenuEvent(QContextMenuEvent *event)
{
    qDebug() << "MyGraphicsView::contextMenuEvent() called.";

    // 右クリックされた位置に QGraphicsItem が存在するかどうかを確認します。
    // この例ではビューのメニューのみを扱うため、アイテムの存在は無視します。
    // QGraphicsItem *item = itemAt(event->pos());

    // ビューのコンテキストメニューを作成します
    QMenu menu(this);
    QAction *action1 = menu.addAction("ビューアクション 1");
    QAction *action2 = menu.addAction("ビューアクション 2");
    QAction *action3 = menu.addAction("ビューをクリア");

    // アクションがトリガーされたときの処理(例: デバッグ出力)
    connect(action1, &QAction::triggered, [](){ qDebug() << "ビューアクション 1 が選択されました"; });
    connect(action2, &QAction::triggered, [](){ qDebug() << "ビューアクション 2 が選択されました"; });
    connect(action3, &QAction::triggered, [this](){
        if (scene()) {
            scene()->clear(); // シーン上の全てのアイテムをクリア
            qDebug() << "シーンがクリアされました";
        }
    });

    // メニューを表示します。event->globalPos() でスクリーン座標を指定します。
    menu.exec(event->globalPos());

    // イベントを処理したことをマークします。
    // これにより、イベントが親ウィジェットなどに伝播されるのを防ぎます。
    event->accept();
}

使い方(main.cppまたはメインウィンドウ)

#include <QApplication>
#include <QMainWindow>
#include <QGraphicsScene>
#include "mygraphicsview.h" // 作成したカスタムビュー

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

    QMainWindow window;
    window.setWindowTitle("QGraphicsView コンテキストメニュー例");
    window.resize(800, 600);

    QGraphicsScene *scene = new QGraphicsScene(0, 0, 780, 580); // シーンのサイズを設定

    // ビューを作成し、シーンを関連付けます
    MyGraphicsView *view = new MyGraphicsView(scene);
    view->setRenderHint(QPainter::Antialiasing); // アンチエイリアスを有効に

    window.setCentralWidget(view);
    window.show();

    return a.exec();
}

QGraphicsItem にコンテキストメニューを表示する

このケースでは、QGraphicsViewcontextMenuEvent のデフォルトの伝播メカニズムを利用して、個々の QGraphicsItem が自身のコンテキストメニューを表示します。

myrectitem.h (カスタムの四角形アイテム)

#ifndef MYRECTITEM_H
#define MYRECTITEM_H

#include <QGraphicsRectItem>
#include <QGraphicsSceneContextMenuEvent> // QGraphicsSceneContextMenuEvent を使用するために必要
#include <QMenu>
#include <QAction>
#include <QDebug>

class MyRectItem : public QGraphicsRectItem
{
public:
    explicit MyRectItem(const QRectF &rect, QGraphicsItem *parent = nullptr);

protected:
    // QGraphicsItem::contextMenuEvent をオーバーライドします
    void contextMenuEvent(QGraphicsSceneContextMenuEvent *event) override;
};

#endif // MYRECTITEM_H

myrectitem.cpp

#include "myrectitem.h"

MyRectItem::MyRectItem(const QRectF &rect, QGraphicsItem *parent)
    : QGraphicsRectItem(rect, parent)
{
    // アイテムがコンテキストメニューイベントを受け取るようにフラグを設定します
    // これが非常に重要です。
    setFlag(QGraphicsItem::ItemIsMovable); // 移動可能にする
    setFlag(QGraphicsItem::ItemIsSelectable); // 選択可能にする
    setFlag(QGraphicsItem::ItemSendsContextMenuEvents, true); // コンテキストメニューイベントを送信
}

void MyRectItem::contextMenuEvent(QGraphicsSceneContextMenuEvent *event)
{
    qDebug() << "MyRectItem::contextMenuEvent() called.";

    QMenu menu;
    QAction *deleteAction = menu.addAction("このアイテムを削除");
    QAction *changeColorAction = menu.addAction("色を変更");

    // アクションがトリガーされたときの処理
    connect(deleteAction, &QAction::triggered, [this](){
        if (scene()) {
            scene()->removeItem(this); // シーンからアイテムを削除
            delete this; // アイテムオブジェクトを削除
            qDebug() << "アイテムが削除されました";
        }
    });
    connect(changeColorAction, &QAction::triggered, [this](){
        // ランダムな色に変更
        QColor randomColor(qrand() % 256, qrand() % 256, qrand() % 256);
        setBrush(QBrush(randomColor));
        qDebug() << "アイテムの色が変更されました";
    });

    // メニューを表示します。event->screenPos() でスクリーン座標を指定します。
    // QGraphicsSceneContextMenuEvent::screenPos() は QGraphicsView の globalPos() に相当します。
    menu.exec(event->screenPos());

    // イベントを処理したことをマークします。
    // これにより、イベントが QGraphicsScene や QGraphicsView に伝播されるのを防ぎます。
    event->accept();
}

使い方(main.cppまたはメインウィンドウ)

#include <QApplication>
#include <QMainWindow>
#include <QGraphicsScene>
#include <QGraphicsView> // この例ではMyGraphicsViewではなく通常のQGraphicsViewを使用
#include "myrectitem.h"

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

    QMainWindow window;
    window.setWindowTitle("QGraphicsItem コンテキストメニュー例");
    window.resize(800, 600);

    QGraphicsScene *scene = new QGraphicsScene(0, 0, 780, 580);
    scene->setBackgroundBrush(Qt::lightGray); // シーンの背景色を設定

    // QGraphicsItem をシーンに追加
    MyRectItem *rect1 = new MyRectItem(QRectF(50, 50, 100, 80));
    rect1->setBrush(Qt::blue);
    scene->addItem(rect1);

    MyRectItem *rect2 = new MyRectItem(QRectF(200, 150, 120, 60));
    rect2->setBrush(Qt::green);
    scene->addItem(rect2);

    // QGraphicsView を作成し、シーンを関連付けます
    QGraphicsView *view = new QGraphicsView(scene);
    view->setRenderHint(QPainter::Antialiasing);

    // デフォルトのコンテキストメニューポリシーのままでOK
    // view->setContextMenuPolicy(Qt::DefaultContextMenu); // これがデフォルトです

    window.setCentralWidget(view);
    window.show();

    return a.exec();
}

ビューとアイテムの両方でコンテキストメニューを切り替える (一般的な実装パターン)

これは、最も一般的な実装パターンです。右クリックされた位置にアイテムがある場合はアイテムのメニューを、そうでない場合はビューのメニューを表示します。

mygraphicsview.h (上記と同じ)

#ifndef MYGRAPHICSVIEW_H
#define MYGRAPHICSVIEW_H

#include <QGraphicsView>
#include <QContextMenuEvent>
#include <QMenu>
#include <QAction>
#include <QDebug>
#include "myrectitem.h" // MyRectItem を含める

class MyGraphicsView : public QGraphicsView
{
    Q_OBJECT

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

protected:
    void contextMenuEvent(QContextMenuEvent *event) override;
};

#endif // MYGRAPHICSVIEW_H

mygraphicsview.cpp

#include "mygraphicsview.h"

MyGraphicsView::MyGraphicsView(QWidget *parent)
    : QGraphicsView(parent)
{
    setContextMenuPolicy(Qt::DefaultContextMenu);
}

MyGraphicsView::MyGraphicsView(QGraphicsScene *scene, QWidget *parent)
    : QGraphicsView(scene, parent)
{
    setContextMenuPolicy(Qt::DefaultContextMenu);
}

void MyGraphicsView::contextMenuEvent(QContextMenuEvent *event)
{
    qDebug() << "MyGraphicsView::contextMenuEvent() called.";

    // 右クリックされた位置に QGraphicsItem が存在するかどうかを確認します
    QGraphicsItem *item = itemAt(event->pos());

    if (item) {
        // アイテムが存在する場合、イベントを QGraphicsScene に伝播させ、
        // そこからアイテムの contextMenuEvent() が呼び出されるようにします。
        // これが最も重要で、Qt のイベント伝播メカニズムに委ねる方法です。
        qDebug() << "Item found. Propagating event to scene/item.";
        QGraphicsView::contextMenuEvent(event); // 親クラスのメソッドを呼び出す

        // イベントがアイテムによって処理されたかどうかを確認します。
        // もしアイテムがイベントを accept() した場合、ビューは何もする必要がありません。
        if (event->isAccepted()) {
            qDebug() << "Item handled the context menu event.";
            return; // ここで処理を終了します
        } else {
            qDebug() << "Item did NOT handle the context menu event. Fallback to view menu.";
            // アイテムがイベントを無視した場合(event->ignore())、ビューのメニューを表示します。
            // (これは QGraphicsView::contextMenuEvent() を呼び出した後に item->contextMenuEvent() が
            //  event->ignore() を実行した場合に発生します)
            // このブロックは通常は必要ありませんが、デバッグのために残しています。
        }
    }

    // アイテムが存在しない場合、またはアイテムがイベントを処理しなかった場合、
    // ビュー独自のコンテキストメニューを表示します。
    qDebug() << "Showing view-specific context menu.";
    QMenu menu(this);
    QAction *viewAction1 = menu.addAction("ビュー: 全アイテム選択");
    QAction *viewAction2 = menu.addAction("ビュー: シーンをリセット");

    connect(viewAction1, &QAction::triggered, [this](){
        if (scene()) {
            scene()->clearSelection(); // 全選択解除
            for (QGraphicsItem *item : scene()->items()) {
                item->setSelected(true); // 全てのアイテムを選択
            }
            qDebug() << "全てのアイテムが選択されました";
        }
    });
    connect(viewAction2, &QAction::triggered, [this](){
        if (scene()) {
            scene()->clear();
            // 新しいアイテムをいくつか追加し直す
            MyRectItem *rect1 = new MyRectItem(QRectF(50, 50, 100, 80));
            rect1->setBrush(Qt::blue);
            scene()->addItem(rect1);

            MyRectItem *rect2 = new MyRectItem(QRectF(200, 150, 120, 60));
            rect2->setBrush(Qt::green);
            scene()->addItem(rect2);
            qDebug() << "シーンがリセットされました";
        }
    });

    menu.exec(event->globalPos());
    event->accept(); // ビューがイベントを処理したことを示す
}

myrectitem.hmyrectitem.cpp は、上記シナリオ2と同じものを再利用します。

使い方(main.cppまたはメインウィンドウ)

#include <QApplication>
#include <QMainWindow>
#include <QGraphicsScene>
#include "mygraphicsview.h" // カスタムビューを使用
#include "myrectitem.h"     // カスタムアイテムを使用

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

    QMainWindow window;
    window.setWindowTitle("ビューとアイテムのコンテキストメニュー例");
    window.resize(800, 600);

    QGraphicsScene *scene = new QGraphicsScene(0, 0, 780, 580);
    scene->setBackgroundBrush(Qt::lightGray);

    // QGraphicsItem をシーンに追加
    MyRectItem *rect1 = new MyRectItem(QRectF(50, 50, 100, 80));
    rect1->setBrush(Qt::blue);
    scene->addItem(rect1);

    MyRectItem *rect2 = new MyRectItem(QRectF(200, 150, 120, 60));
    rect2->setBrush(Qt::green);
    scene->addItem(rect2);

    MyRectItem *rect3 = new MyRectItem(QRectF(350, 100, 70, 70));
    rect3->setBrush(Qt::red);
    scene->addItem(rect3);

    // カスタムビューを作成し、シーンを関連付けます
    MyGraphicsView *view = new MyGraphicsView(scene);
    view->setRenderHint(QPainter::Antialiasing);

    window.setCentralWidget(view);
    window.show();

    return a.exec();
}

これらの例をコンパイルするには、.pro ファイルに以下の行を追加し、Qt Creator または QMake を使用してビルドします。

QT += widgets
SOURCES += main.cpp \
           mygraphicsview.cpp \
           myrectitem.cpp
HEADERS += mygraphicsview.h \
           myrectitem.h
  • setFlag(QGraphicsItem::ItemSendsContextMenuEvents, true): QGraphicsItem がコンテキストメニューイベントを受け取るようにするために、このフラグを設定することが重要です。
  • event->accept() / event->ignore(): イベントが処理されたかどうかをQtのイベントシステムに伝えるために重要です。
    • メニューを表示して処理を完了した場合は event->accept() を呼び出します。
    • イベントを次のハンドラ(親ウィジェット、またはデフォルトの処理)に任せる場合は event->ignore() を呼び出します。
  • 座標系:
    • QMenu::exec() には、スクリーングローバル座標 (event->globalPos() または event->screenPos()) を渡すのが一般的です。
    • QGraphicsView::contextMenuEvent() では QContextMenuEvent::globalPos() を使用します。
    • QGraphicsItem::contextMenuEvent() では QGraphicsSceneContextMenuEvent::screenPos() を使用します。
  • itemAt(event->pos()): QGraphicsView のメソッドで、ビューポートのローカル座標 event->pos() にある最上位の QGraphicsItem を返します。これを使って、クリックされた位置にアイテムがあるかを判断します。
  • イベントの伝播:
    • QGraphicsView::contextMenuEvent() のデフォルト実装は、イベントを QGraphicsScene に転送します。
    • QGraphicsScene は、イベント発生位置に QGraphicsItem がある場合、そのアイテムの contextMenuEvent() を呼び出します。
    • アイテムが event->accept() を呼び出すと、イベントの伝播は停止し、ビューはメニューを表示しません。
    • アイテムが event->ignore() を呼び出す(または何も処理しない)と、イベントはビューに戻り、ビューが独自のメニューを表示する機会を得ます。
  • QGraphicsItem::contextMenuEvent(QGraphicsSceneContextMenuEvent *event): これは QGraphicsItem の仮想関数で、シーン上のアイテムに対するコンテキストメニューイベントを扱います。QGraphicsSceneContextMenuEvent は、シーン固有の情報(シーン座標など)を持つ QContextMenuEvent のサブクラスです。
  • QGraphicsView::contextMenuEvent(QContextMenuEvent *event): これは QWidget から継承された仮想関数で、ビューポート上でのコンテキストメニューイベントを扱います。


Qt::CustomContextMenu ポリシーと customContextMenuRequested シグナル

これは、contextMenuEvent() をオーバーライドする一般的な代替手段であり、特にQt DesignerでUIを設計する際に便利です。

仕組み

  • このシグナルをカスタムスロットに接続し、そのスロット内でメニューを作成・表示するロジックを実装します。
  • これにより、ウィジェット(この場合は QGraphicsView のビューポート)がコンテキストメニューイベントを受け取ったときに、customContextMenuRequested(const QPoint &pos) シグナルを発行するようになります。
  • QWidget::contextMenuPolicy プロパティを Qt::CustomContextMenu に設定します。

利点

  • イベントの伝播の管理
    contextMenuEvent() の複雑なイベント伝播ロジックを直接管理する必要がなくなります。customContextMenuRequested シグナルは、ウィジェット自身がコンテキストメニューのトリガーを受け取ったことを示します。
  • Qt Designerとの統合
    Qt Designerでウィジェットのプロパティとして設定し、自動的にスロットを生成できます。
  • 簡潔さ
    ウィジェットのサブクラス化やイベントハンドラのオーバーライドが不要になります。

欠点

  • QGraphicsItem には customContextMenuRequested シグナルがないため、アイテムのコンテキストメニューはこの方法では直接扱えません。アイテムのメニューは、依然として QGraphicsItem::contextMenuEvent() をオーバーライドする必要があります。
  • QGraphicsView の場合、itemAt() などを使って右クリックされた位置にアイテムがあるかどうかを判断し、アイテムのメニューとビューのメニューを切り替えるロジックを自分で実装する必要があります。

コード例

// MyGraphicsView.h
#ifndef MYGRAPHICSVIEW_H
#define MYGRAPHICSVIEW_H

#include <QGraphicsView>
#include <QMenu>
#include <QAction>
#include <QPoint> // QPoint を使用するために必要
#include <QDebug>
#include "myrectitem.h" // アイテムのメニューを処理する場合

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

private slots:
    // customContextMenuRequested シグナルに接続するスロット
    void showCustomContextMenu(const QPoint &pos);
};

#endif // MYGRAPHICSVIEW_H
// MyGraphicsView.cpp
#include "mygraphicsview.h"

MyGraphicsView::MyGraphicsView(QWidget *parent)
    : QGraphicsView(parent)
{
    // コンテキストメニューポリシーをカスタムに設定
    setContextMenuPolicy(Qt::CustomContextMenu);
    // シグナルとスロットを接続
    connect(this, &QGraphicsView::customContextMenuRequested,
            this, &MyGraphicsView::showCustomContextMenu);
}

MyGraphicsView::MyGraphicsView(QGraphicsScene *scene, QWidget *parent)
    : QGraphicsView(scene, parent)
{
    setContextMenuPolicy(Qt::CustomContextMenu);
    connect(this, &QGraphicsView::customContextMenuRequested,
            this, &MyGraphicsView::showCustomContextMenu);
}

void MyGraphicsView::showCustomContextMenu(const QPoint &pos)
{
    qDebug() << "MyGraphicsView::showCustomContextMenu() called at" << pos;

    // ビューポート座標 (pos) をシーン座標に変換
    QPointF scenePos = mapToScene(pos);

    // シーン座標にあるアイテムを取得
    QGraphicsItem *item = scene()->itemAt(scenePos, transform());

    if (item) {
        // アイテムが存在する場合、アイテムのコンテキストメニューを直接呼び出す
        // QGraphicsItem の contextMenuEvent は QGraphicsSceneContextMenuEvent を期待します
        // そのため、適切なイベントオブジェクトを構築する必要があります。
        QGraphicsSceneContextMenuEvent customEvent(QEvent::GraphicsSceneContextMenu);
        customEvent.setPos(scenePos); // シーン座標
        customEvent.setScreenPos(mapToGlobal(pos)); // スクリーン座標

        // アイテムにイベントを送信
        // アイテムがイベントを処理したかどうかは isAccepted() で確認できる
        if (item->sceneEvent(&customEvent)) { // item->contextMenuEvent() を直接呼び出すのではなく、sceneEvent() を使う
            qDebug() << "Item handled the context menu.";
            if (customEvent.isAccepted()) {
                return; // アイテムがメニューを表示し、処理を完了した
            }
        }
        // アイテムがイベントを処理しなかった(または isAccepted() を呼び出さなかった)場合、
        // フォールバックしてビューのメニューを表示する
        qDebug() << "Item did not handle or accept. Showing view menu.";
    }

    // ビューのコンテキストメニューを作成して表示
    QMenu menu(this);
    menu.addAction("ビューアクション (カスタム)");
    menu.addAction("シーンをクリア (カスタム)");
    // ... その他のビューアクション ...

    menu.exec(mapToGlobal(pos)); // グローバル座標でメニューを表示
}

myrectitem.hmyrectitem.cpp は、以前の例と同じものを使用します。

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

イベントフィルタは、特定のオブジェクトに送られるすべてのイベントを監視・処理するための強力なメカニズムです。これにより、サブクラス化せずに既存のウィジェットの動作を変更できます。

仕組み

  • イベントを処理しない場合は false を返し、通常のイベント処理パスに沿って伝播させます。
  • イベントを処理した場合は true を返し、それ以上伝播させないようにします。
  • このメソッド内で、QEvent::ContextMenu 型のイベントをチェックし、必要な処理を行います。
  • イベントフィルタオブジェクト(通常は QObject のサブクラス)で eventFilter(QObject *watched, QEvent *event) メソッドをオーバーライドします。
  • 監視したいオブジェクト (例: QGraphicsView のビューポート) に installEventFilter() を呼び出してイベントフィルタオブジェクトをインストールします。

利点

  • 分離
    イベント処理ロジックを独立したクラスに分離できます。
  • 汎用性
    任意の QObject に対してイベントフィルタをインストールでき、様々な種類のイベントを監視・処理できます。
  • 非侵襲的
    既存のクラスをサブクラス化する必要がないため、Qtの提供する標準ウィジェットをそのまま利用できます。

欠点

  • QGraphicsView のイベント伝播を理解していないと、意図しない挙動を引き起こす可能性があります。特に、QGraphicsItem のコンテキストメニューと共存させる場合は、イベントフィルタ内で QGraphicsView::contextMenuEvent(event) を明示的に呼び出すなどの工夫が必要です。
  • コードが複雑になる傾向があります。

コード例

// MyEventFilter.h
#ifndef MYEVENTFILTER_H
#define MYEVENTFILTER_H

#include <QObject>
#include <QEvent>
#include <QContextMenuEvent>
#include <QGraphicsView>
#include <QGraphicsScene>
#include <QMenu>
#include <QAction>
#include <QDebug>
#include "myrectitem.h" // アイテムのメニューを処理する場合

class MyEventFilter : public QObject
{
    Q_OBJECT
public:
    explicit MyEventFilter(QObject *parent = nullptr, QGraphicsView *view = nullptr);

protected:
    bool eventFilter(QObject *watched, QEvent *event) override;

private:
    QGraphicsView *m_view; // フィルタリング対象のビュー
};

#endif // MYEVENTFILTER_H
// MyEventFilter.cpp
#include "myeventfilter.h"

MyEventFilter::MyEventFilter(QObject *parent, QGraphicsView *view)
    : QObject(parent), m_view(view)
{
}

bool MyEventFilter::eventFilter(QObject *watched, QEvent *event)
{
    if (watched == m_view->viewport() && event->type() == QEvent::ContextMenu) {
        QContextMenuEvent *contextMenuEvent = static_cast<QContextMenuEvent *>(event);
        qDebug() << "MyEventFilter: ContextMenuEvent captured for viewport.";

        // QGraphicsViewのデフォルトの contextMenuEvent 処理を呼び出して
        // アイテムにイベントを伝播させます。
        // これがないと、アイテムのコンテキストメニューが表示されません。
        m_view->contextMenuEvent(contextMenuEvent);

        // イベントがアイテムによって処理されたかどうかを確認
        if (contextMenuEvent->isAccepted()) {
            qDebug() << "MyEventFilter: Event was handled by an item.";
            return true; // アイテムが処理したので、フィルタはイベントを消費する
        } else {
            // アイテムが処理しなかった場合、またはアイテムがない場合、
            // ビューのコンテキストメニューを表示
            qDebug() << "MyEventFilter: Item did not handle. Showing view-specific menu.";
            QMenu menu(m_view); // 親をビューに設定
            menu.addAction("ビューアクション (フィルタ)");
            menu.addAction("フィルタからシーンをクリア");

            connect(menu.actions().first(), &QAction::triggered, [](){ qDebug() << "ビューアクション (フィルタ) 選択"; });
            connect(menu.actions().last(), &QAction::triggered, [this](){
                if (m_view && m_view->scene()) {
                    m_view->scene()->clear();
                    qDebug() << "シーンがフィルタからクリアされました";
                }
            });

            menu.exec(contextMenuEvent->globalPos());
            return true; // フィルタがイベントを処理したので、これ以上伝播させない
        }
    }

    // 他のイベントは親のイベントフィルタに渡すか、デフォルトの処理に戻す
    return QObject::eventFilter(watched, event);
}
#include <QApplication>
#include <QMainWindow>
#include <QGraphicsScene>
#include "mygraphicsview.h" // MyGraphicsView は、この例では setContextMenuPolicy(Qt::DefaultContextMenu) に戻しておくか、何もしないシンプルなものにする
#include "myrectitem.h"
#include "myeventfilter.h" // 作成したイベントフィルタ

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

    QMainWindow window;
    window.setWindowTitle("イベントフィルタによるコンテキストメニュー例");
    window.resize(800, 600);

    QGraphicsScene *scene = new QGraphicsScene(0, 0, 780, 580);
    scene->setBackgroundBrush(Qt::lightGray);

    MyRectItem *rect1 = new MyRectItem(QRectF(50, 50, 100, 80));
    rect1->setBrush(Qt::blue);
    scene->addItem(rect1);

    MyRectItem *rect2 = new MyRectItem(QRectF(200, 150, 120, 60));
    rect2->setBrush(Qt::green);
    scene->addItem(rect2);

    MyGraphicsView *view = new MyGraphicsView(scene); // ここではシンプルなQGraphicsViewか、contextMenuPolicyをDefaultにしたMyGraphicsView
    view->setRenderHint(QPainter::Antialiasing);

    // イベントフィルタをビューのビューポートにインストール
    MyEventFilter *eventFilter = new MyEventFilter(&window, view); // 親をwindowに設定
    view->viewport()->installEventFilter(eventFilter);
    // QGraphicsViewのコンテキストメニューポリシーは DefaultContextMenu のままでOKです。
    // イベントフィルタが ContextMenuEvent を横取りし、必要に応じて
    // view->contextMenuEvent() を呼び出してアイテムにイベントを伝播させます。
    // view->setContextMenuPolicy(Qt::DefaultContextMenu); // これがデフォルト動作

    window.setCentralWidget(view);
    window.show();

    return a.exec();
}
  • 既存のクラスを変更せずに動作を拡張したい場合や、複雑なイベント処理が必要な場合
    イベントフィルタを使用します。しかし、これはより低レベルで、QGraphicsフレームワークのイベント伝播の仕組みをより深く理解する必要があります。
  • Qt Designerを多用する場合やシンプルなメニューの場合
    Qt::CustomContextMenu ポリシーと customContextMenuRequested シグナルを使用します。ただし、アイテムのメニューとビューのメニューを切り替えるロジックは手動で実装する必要があります。
  • 最も一般的で推奨される方法
    QGraphicsView::contextMenuEvent() をオーバーライドし、その中で itemAt() を使ってアイテムの有無をチェックし、適切なメニューを表示する(「ビューとアイテムの両方でコンテキストメニューを切り替える」の例を参照)。これは、QGraphicsフレームワークのイベント伝播を最も自然に利用できます。