初心者向けQt Graphics View: contextMenuEventでリッチな右クリックメニューを実装
役割
QGraphicsView
は QGraphicsScene
の内容を表示するためのウィジェットです。ユーザーが QGraphicsView
の上で右クリック(またはコンテキストメニューをトリガーする操作)を行った際に、この contextMenuEvent
が発生します。
このイベントハンドラの主な役割は、以下の通りです。
- ビューのコンテキストメニュー処理:
QGraphicsView
自体に対してコンテキストメニューを表示したい場合に、この関数を再実装(オーバーライド)します。 - イベントの伝播: デフォルトの実装では、このイベントはビューが管理する
QGraphicsScene
に転送され、さらにシーン上のQGraphicsItem
に伝播されます。これにより、シーン上の特定のアイテムに対して個別のコンテキストメニューを表示することができます。
動作の仕組み
- 右クリックの検出: ユーザーが
QGraphicsView
のどこかを右クリックすると、QGraphicsView
はQContextMenuEvent
を受け取ります。 contextMenuEvent
の呼び出し:QGraphicsView
は、このイベントを受け取ると、自身のcontextMenuEvent(QContextMenuEvent *event)
メソッドを呼び出します。- デフォルトの挙動:
QGraphicsView
のデフォルトのcontextMenuEvent
実装は、このイベントをQGraphicsScene
に転送します。QGraphicsScene
は、イベント発生位置にQGraphicsItem
があるかどうかを調べます。- アイテムが存在する場合、そのアイテムの
contextMenuEvent(QGraphicsSceneContextMenuEvent *event)
が呼び出されます。 - もし、どのアイテムもイベントを処理しなかった場合(
event->ignore()
を呼び出した場合)、またはイベント発生位置にアイテムがなかった場合、シーンはイベントを無視し、イベントは最終的にビューに戻ってきます。 - ビューは、そのイベントがまだ処理されていない場合、自身のコンテキストメニューを表示する機会を得ます。
- 再実装(オーバーライド):
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)
を呼び出すことで、アイテムのコンテキストメニューが優先的に表示されるようにしています。
QWidget
(QGraphicsView
も継承しています)には contextMenuPolicy
というプロパティがあります。これは、コンテキストメニューイベントをどのように処理するかを制御します。
Qt::CustomContextMenu
:contextMenuEvent()
は呼び出されず、代わりにcustomContextMenuRequested(const QPoint &pos)
シグナルが発せられます。このシグナルを接続して、カスタムのコンテキストメニューロジックを実装することもできます。Qt::DefaultContextMenu
(デフォルト):contextMenuEvent()
ハンドラが呼び出されます。上記で説明したような、イベントの伝播が行われます。
多くの場合、QGraphicsView
のcontextMenuEvent
をオーバーライドして、イベントの伝播とビュー独自のメニューを両方処理する方法が一般的です。しかし、よりシンプルなカスタムメニューが必要な場合は、Qt::CustomContextMenu
ポリシーとシグナルを使用することもできます。
QGraphicsView
のコンテキストメニューイベントは、QGraphicsScene
や QGraphicsItem
との連携が複雑になるため、いくつかの一般的な落とし穴があります。
ビューのコンテキストメニューが表示されない、またはアイテムのメニューが表示されない
原因
- アイテムの contextMenuEvent で event->ignore() を呼び出している
QGraphicsItem
のcontextMenuEvent
でevent->ignore()
を呼び出すと、イベントはシーンに、さらにビューに伝播されます。意図せずignore()
している場合、メニューが表示されないことがあります。 - デフォルトの実装の呼び出し忘れ
QGraphicsView::contextMenuEvent(event)
をオーバーライドした際に、親クラスのメソッドを呼び出し忘れている。これにより、イベントがQGraphicsScene
やQGraphicsItem
に適切に伝播されません。 - イベントの伝播の停止
contextMenuEvent()
をオーバーライドした際に、event->accept()
を呼び出してイベントの伝播を早期に停止してしまっている。これにより、QGraphicsScene
やQGraphicsItem
にイベントが届かなくなります。 - Qt::ContextMenuPolicy の設定ミス
QGraphicsView
のcontextMenuPolicy
がQt::CustomContextMenu
に設定されており、customContextMenuRequested
シグナルにスロットが接続されていない、またはそのスロット内でコンテキストメニューが表示されていない。この場合、contextMenuEvent()
は呼び出されません。
トラブルシューティング
- イベントの伝播の確認
QGraphicsView::contextMenuEvent()
をオーバーライドする際、アイテムがある場合はQGraphicsView::contextMenuEvent(event);
を呼び出すようにしてください。これにより、イベントがシーンとアイテムに適切に伝播されます。- アイテムがない場合にビューのメニューを表示し、その際に
event->accept()
を呼び出すようにします。 QGraphicsItem
のcontextMenuEvent()
内で、意図しないevent->ignore()
がないか確認してください。
- contextMenuPolicy の確認
QGraphicsView
のsetContextMenuPolicy(Qt::DefaultContextMenu)
を明示的に設定してみてください。これにより、contextMenuEvent()
が確実に呼び出されます。Qt::CustomContextMenu
を使用する場合は、必ずcustomContextMenuRequested
シグナルを適切なスロットに接続し、そのスロット内でメニューを表示するロジックを実装してください。
常にビューのコンテキストメニューが表示される
原因
- QGraphicsView::contextMenuEvent() の実装でアイテムチェックをしていない
QGraphicsView
のオーバーライドされたcontextMenuEvent()
内で、右クリックされた位置にアイテムがあるかどうかをチェックせずに、常にビューのメニューを表示するロジックになっている。 - アイテムの contextMenuEvent が処理されていない
QGraphicsItem
のcontextMenuEvent()
が適切に実装されていないか、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());
特定のアイテムだけメニューが表示されない
原因
- イベントフィルタの影響
QGraphicsView
やQGraphicsScene
、あるいは親ウィジェットにイベントフィルタがインストールされており、コンテキストメニューイベントを横取りしてしまっている。 - アイテムの contextMenuEvent() がオーバーライドされていない
そもそも、その特定のアイテムのサブクラスでcontextMenuEvent()
が実装されていない。 - アイテムのクリック可能/選択可能設定
QGraphicsItem::setFlags()
でItemIsSelectable
やItemIsMovable
など、イベントを受け取るための適切なフラグが設定されていない。
トラブルシューティング
- イベントフィルタの確認
- もしイベントフィルタを使っている場合、そのフィルタがコンテキストメニューイベントを不適切に処理していないかデバッグで追ってみてください。
- アイテムのオーバーライドを確認
- そのアイテムのクラス定義で
void contextMenuEvent(QGraphicsSceneContextMenuEvent *event) override;
が適切に宣言・実装されているか確認します。
- そのアイテムのクラス定義で
- アイテムのフラグを確認
- 対象の
QGraphicsItem
がイベントを受け取れる状態にあるか、setFlag(QGraphicsItem::ItemSendsContextMenuEvents, true)
や他の関連するフラグが設定されているか確認します。
- 対象の
コンテキストメニューが表示された後、他のマウスイベントが反応しない
原因
- イベントのロック
QMenu::exec()
はモーダルなイベントループを開始するため、メニューが閉じられるまで他のマウスイベントが処理されないのが通常の挙動です。これはエラーではなく、期待される動作です。
- これは通常のエラーではありません。ユーザーがメニューから選択を行うか、メニューの外をクリックしてメニューが閉じられるまで、他のマウスイベントはブロックされます。
デバッグのヒント
- Qt のドキュメントを参照する
QGraphicsView
,QGraphicsScene
,QGraphicsItem
のcontextMenuEvent
に関する公式ドキュメントは非常に詳細で、実装のベストプラクティスが記載されています。 - イベントオブジェクトの内容を確認する
QContextMenuEvent
やQGraphicsSceneContextMenuEvent
のpos()
,globalPos()
,reason()
などのプロパティをデバッグ出力して、イベントの状態を把握します。 - qDebug() を活用する
contextMenuEvent()
やQGraphicsItem
のcontextMenuEvent()
の内部にqDebug()
を仕込み、イベントがどの経路をたどっているか、どの時点でaccept()
/ignore()
されているかを確認します。
- QGraphicsView 自体にコンテキストメニューを表示する
- QGraphicsItem にコンテキストメニューを表示する (QGraphicsView からの伝播を利用)
- ビューとアイテムの両方でコンテキストメニューを切り替える (一般的な実装パターン)
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 にコンテキストメニューを表示する
このケースでは、QGraphicsView
の contextMenuEvent
のデフォルトの伝播メカニズムを利用して、個々の 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.h
と myrectitem.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.h
と myrectitem.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
フレームワークのイベント伝播を最も自然に利用できます。