【初心者向け】Qt QGraphicsScene マウスイベントのプログラミング入門
QGraphicsScene::mousePressEvent() とは
QGraphicsScene::mousePressEvent()
は、Qtのグラフィックビューフレームワークにおいて、QGraphicsScene
クラスに属する仮想関数の一つです。この関数は、シーン(QGraphicsScene
オブジェクト)内の何らかのアイテム上でマウスボタンが押された際に呼び出されます。
役割と機能
このイベントハンドラの主な役割は、マウスのプレス操作に応じた処理を実装することです。具体的には、以下の様な処理を行うことができます。
- カスタムなインタラクションの実装
アプリケーション固有の、マウスプレスによる特別な動作を定義できます。 - アイテムの選択や移動の開始
マウスプレスをトリガーとして、アイテムの選択状態を変更したり、ドラッグ操作を開始したりする処理を実装できます。 - 押されたアイテムの特定
マウスボタンが押された位置にあるグラフィックアイテム(QGraphicsItem
の派生クラスのインスタンス)を特定できます。 - マウスカーソルの位置の取得
シーン座標系におけるマウスカーソルの位置を取得できます。 - マウスボタンの種類の特定
どのマウスボタン(左、右、中央など)が押されたのかを判別できます。
イベントの流れ
- ユーザーがグラフィックビュー(
QGraphicsView
)上でマウスボタンを押します。 - グラフィックビューは、そのマウスイベントを関連付けられているシーン(
QGraphicsScene
)に伝播させます。 - シーンは、マウスイベントが発生した位置にあるアイテムを特定します。
- もしその位置にグラフィックアイテムが存在し、そのアイテムがマウスイベントを受け付ける設定になっている場合(通常はデフォルトで受け付けます)、そのアイテムの
mousePressEvent()
関数が最初に呼び出されます。 - もしアイテムがそのイベントを処理しなかった場合(イベントオブジェクトの
accept()
を呼ばなかった場合)、シーン自身のmousePressEvent()
関数が呼び出されます。
実装方法
QGraphicsScene
クラスを直接サブクラス化し、その中で mousePressEvent()
関数をオーバーライドすることで、マウスプレスイベントに対するカスタムな処理を実装できます。
以下は、簡単な実装例です。
#include <QGraphicsScene>
#include <QGraphicsRectItem>
#include <QGraphicsView>
#include <QMouseEvent>
#include <QDebug>
class MyScene : public QGraphicsScene {
public:
MyScene(QObject *parent = nullptr) : QGraphicsScene(parent) {
// シーンに矩形アイテムを追加
addItem(new QGraphicsRectItem(0, 0, 100, 100));
}
protected:
void mousePressEvent(QGraphicsSceneMouseEvent *event) override {
// マウスボタンの種類と位置を表示
qDebug() << "Mouse pressed at:" << event->scenePos() << ", button:" << event->button();
// クリックされたアイテムを取得
QGraphicsItem *item = itemAt(event->scenePos(), QTransform());
if (item) {
qDebug() << "Item clicked:" << item;
// アイテムに対する何らかの処理(例:選択状態の変更)
item->setSelected(true);
}
// 親クラスの mousePressEvent() を呼び出すことで、デフォルトの処理も実行される
QGraphicsScene::mousePressEvent(event);
}
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MyScene scene;
QGraphicsView view(&scene);
view.show();
return a.exec();
}
この例では、MyScene
クラスが QGraphicsScene
を継承し、mousePressEvent()
をオーバーライドしています。マウスボタンが押されると、その位置とボタンの種類がコンソールに出力され、クリックされたアイテムがあれば、その情報も出力され、選択状態になります。
- 通常は、親クラスの
mousePressEvent()
を呼び出すことで、デフォルトのイベント処理も実行するようにします。 - イベントを処理したら、必要に応じてイベントオブジェクトの
accept()
を呼び出すことができます。これにより、イベントがそれ以上伝播するのを防ぐことができます。 itemAt()
関数を使用すると、指定されたシーン座標にあるグラフィックアイテムを取得できます。
よくあるエラーとトラブルシューティング
QGraphicsScene::mousePressEvent()
を扱う際に遭遇しやすいエラーとその解決策を以下に示します。
mousePressEvent() が呼び出されない
- トラブルシューティング
QGraphicsView
の設定を確認する。特に、setMouseTracking(true)
の設定が意図しない動作を引き起こしていないか確認する(通常、プレスイベントには影響しませんが、他のマウスイベントとの関連で考慮が必要な場合があります)。- シーンまたは関連するアイテムの
setAcceptedMouseButtons()
関数を確認し、必要なボタンが受け付けられているか確認する。デフォルトでは全てのマウスボタンを受け付けます。 QGraphicsItem::setFlag(QGraphicsItem::ItemIsSelectable)
やQGraphicsItem::setFlag(QGraphicsItem::ItemIsMovable)
などのフラグが、意図せずマウスイベントの処理を阻害していないか確認する。これらのフラグは内部でマウスイベントを処理することがあります。- 前面に重なっているアイテムがないか確認する。アイテムの Z 値(
setZValue()
)を調整して、意図したアイテムがマウスイベントを受け取れるようにする。 - 親の
QGraphicsItem
がマウスイベントを処理し、子アイテムに伝播していない場合がある。親アイテムのmousePressEvent()
の実装を確認する。
- 原因
QGraphicsView
がマウスイベントをシーンに正しく伝播していない。- シーンまたはアイテムがマウスイベントを受け付ける設定になっていない。
- マウスイベントを遮る別のアイテムが前面に存在している。
クリックされたアイテムが意図したものと異なる
- トラブルシューティング
event->scenePos()
を使用してシーン座標を取得していることを確認する。ビュー座標を使用している場合は、view->mapToScene(event->pos())
でシーン座標に変換する。- 重なり合うアイテムの Z 値を確認し、意図した順序になっているか確認する。
- カスタムアイテムの場合、
boundingRect()
とshape()
の実装が正しいか確認する。shape()
はより正確な当たり判定に使用されます。 items()
関数を使って、特定の位置に存在する全てのアイテムを取得し、デバッグ出力などで確認する。
- 原因
itemAt()
関数に渡す座標が間違っている。シーン座標系とビュー座標系を混同している可能性がある。- 複数のアイテムが重なっており、意図しないアイテムが
itemAt()
で返されている。 - アイテムの形状(bounding rect や shape())が実際の描画と異なっている。
マウスボタンの種類を正しく判別できない
- トラブルシューティング
event->button()
は、最後に押されたボタンを示します。複数のボタンの状態を確認したい場合は、event->buttons()
を使用します。これはQt::MouseButtons
型のビットマスクを返します。- ビット演算子(
&
)を使って、特定のボタンが押されているか確認します(例:if (event->buttons() & Qt::LeftButton)
)。
- 原因
event->button()
の戻り値を正しく評価していない。- 複数のボタンが同時に押された場合の処理が考慮されていない。
マウスカーソルの位置が期待通りでない
- トラブルシューティング
event->scenePos()
はシーン座標、event->pos()
はビュー座標、QCursor::pos()
はグローバルスクリーン座標を返します。目的に合った座標系を使用しているか確認する。- アイテムの座標変換(
item->mapToScene()
,item->mapFromScene()
など)を使用して、必要に応じて座標を変換する。
- 原因
- 座標系の誤解(シーン座標 vs. ビュー座標 vs. グローバルスクリーン座標)。
- アイテムの変形(回転、スケールなど)が考慮されていない。
イベントの伝播が意図通りに行かない
- トラブルシューティング
- アイテムがイベントを処理する必要がある場合にのみ
event->accept()
を呼び出すようにする。デフォルトの処理も行いたい場合は、QGraphicsItem::mousePressEvent()
の最後にQGraphicsScene::mousePressEvent(event)
を呼び出す(または、アイテムで処理しない場合はignore()
を呼ぶ)。 - 親アイテムのイベントハンドラの実装を確認し、子アイテムにイベントが適切に伝播しているか確認する。
- アイテムがイベントを処理する必要がある場合にのみ
- 原因
- アイテムの
mousePressEvent()
でevent->accept()
を呼びすぎて、シーンのmousePressEvent()
が呼ばれない。 - 親アイテムがイベントを横取りしている。
- アイテムの
- グラフィックビューの可視化
アイテムの形状や位置が期待通りであるか、グラフィックビューの描画を確認します。 - ブレークポイントの設定
IDE のデバッガを使用してmousePressEvent()
内にブレークポイントを設定し、ステップ実行しながら変数の値を確認します。 - qDebug() の使用
マウスイベントが発生した位置、ボタンの種類、クリックされたアイテムなどをqDebug()
で出力して、イベントの流れやデータの状態を確認します。
基本的な例:クリック位置とボタンの情報を表示する
これは、マウスがクリックされた位置(シーン座標)と押されたボタンの種類をコンソールに出力する基本的な例です。
#include <QApplication>
#include <QGraphicsScene>
#include <QGraphicsView>
#include <QMouseEvent>
#include <QDebug>
class MyScene : public QGraphicsScene {
public:
MyScene(QObject *parent = nullptr) : QGraphicsScene(parent) {}
protected:
void mousePressEvent(QGraphicsSceneMouseEvent *event) override {
// クリックされたシーン座標を表示
qDebug() << "マウスがクリックされました (シーン座標):" << event->scenePos();
// 押されたマウスボタンの種類を表示
if (event->button() == Qt::LeftButton) {
qDebug() << "左ボタン";
} else if (event->button() == Qt::RightButton) {
qDebug() << "右ボタン";
} else if (event->button() == Qt::MiddleButton) {
qDebug() << "中央ボタン";
}
// 親クラスの mousePressEvent() を呼び出すことで、デフォルトの処理も継続する
QGraphicsScene::mousePressEvent(event);
}
};
int main(int argc, char *argv[]) {
QApplication a(argc, argv);
MyScene scene;
scene.addText("シーンをクリックしてください");
QGraphicsView view(&scene);
view.show();
return a.exec();
}
この例では、MyScene
クラスが QGraphicsScene
を継承し、mousePressEvent()
をオーバーライドしています。マウスがクリックされると、event->scenePos()
でシーン座標を取得し、event->button()
で押されたボタンの種類を判別して出力します。
例2:クリックした位置に円を描画する
マウスがクリックされた位置に新しい円のグラフィックアイテムを追加する例です。
#include <QApplication>
#include <QGraphicsScene>
#include <QGraphicsView>
#include <QGraphicsEllipseItem>
#include <QMouseEvent>
class MyScene : public QGraphicsScene {
public:
MyScene(QObject *parent = nullptr) : QGraphicsScene(parent) {}
protected:
void mousePressEvent(QGraphicsSceneMouseEvent *event) override {
// クリックされた位置に半径10の円を作成
QGraphicsEllipseItem *circle = new QGraphicsEllipseItem(QRectF(-10, -10, 20, 20));
circle->setPos(event->scenePos());
addItem(circle);
// 親クラスの mousePressEvent() を呼び出す(必要に応じて)
QGraphicsScene::mousePressEvent(event);
}
};
int main(int argc, char *argv[]) {
QApplication a(argc, argv);
MyScene scene;
QGraphicsView view(&scene);
view.setScene(&scene); // 明示的にシーンを設定
view.show();
return a.exec();
}
この例では、クリックされるたびに新しい QGraphicsEllipseItem
が作成され、クリックされたシーン座標に配置されてシーンに追加されます。
例3:クリックしたアイテムを選択状態にする
マウスがクリックされたアイテムを選択状態にする例です。
#include <QApplication>
#include <QGraphicsScene>
#include <QGraphicsView>
#include <QGraphicsRectItem>
#include <QMouseEvent>
#include <QDebug>
class MyScene : public QGraphicsScene {
public:
MyScene(QObject *parent = nullptr) : QGraphicsScene(parent) {
// いくつかの矩形アイテムを追加
addItem(new QGraphicsRectItem(0, 0, 50, 50));
addItem(new QGraphicsRectItem(60, 0, 50, 50));
addItem(new QGraphicsRectItem(0, 60, 50, 50));
}
protected:
void mousePressEvent(QGraphicsSceneMouseEvent *event) override {
// クリックされた位置にあるアイテムを取得
QGraphicsItem *item = itemAt(event->scenePos(), QTransform());
if (item) {
// アイテムを選択状態にする
item->setSelected(true);
qDebug() << "アイテムが選択されました:" << item;
} else {
// アイテムがクリックされなかった場合は、全てのアイテムの選択を解除する
clearSelection();
qDebug() << "アイテムが選択解除されました";
}
// 親クラスの mousePressEvent() を呼び出す
QGraphicsScene::mousePressEvent(event);
}
};
int main(int argc, char *argv[]) {
QApplication a(argc, argv);
MyScene scene;
QGraphicsView view(&scene);
view.setScene(&scene);
view.setRenderHints(QPainter::Antialiasing); // 描画を滑らかにする(任意)
view.show();
return a.exec();
}
この例では、itemAt()
関数を使用してクリックされた位置にあるアイテムを取得し、そのアイテムの setSelected(true)
を呼び出して選択状態にします。アイテムがクリックされなかった場合は、clearSelection()
を呼び出して全てのアイテムの選択を解除します。
例4:右クリックでアイテムを削除する
右クリックされたアイテムをシーンから削除する例です。
#include <QApplication>
#include <QGraphicsScene>
#include <QGraphicsView>
#include <QGraphicsRectItem>
#include <QMouseEvent>
#include <QDebug>
class MyScene : public QGraphicsScene {
public:
MyScene(QObject *parent = nullptr) : QGraphicsScene(parent) {
addItem(new QGraphicsRectItem(0, 0, 50, 50));
addItem(new QGraphicsRectItem(60, 0, 50, 50));
}
protected:
void mousePressEvent(QGraphicsSceneMouseEvent *event) override {
if (event->button() == Qt::RightButton) {
// 右クリックされた位置にあるアイテムを取得
QGraphicsItem *item = itemAt(event->scenePos(), QTransform());
if (item) {
// シーンからアイテムを削除
removeItem(item);
delete item; // メモリリークを防ぐために delete する
qDebug() << "アイテムが削除されました";
return; // イベント処理をここで終了
}
}
// 左クリックなどの場合は、親クラスの mousePressEvent() を呼び出す
QGraphicsScene::mousePressEvent(event);
}
};
int main(int argc, char *argv[]) {
QApplication a(argc, argv);
MyScene scene;
QGraphicsView view(&scene);
view.setScene(&scene);
view.show();
return a.exec();
}
この例では、右クリック(event->button() == Qt::RightButton
)が検出された場合に、クリックされた位置のアイテムを取得し、removeItem()
でシーンから削除し、さらに delete
でメモリを解放しています。
QGraphicsItem::mousePressEvent() をオーバーライドする
シーン全体のマウスプレスイベントを処理するのではなく、特定のアイテムに対するマウスプレスイベントのみを処理したい場合に有効です。
- 欠点
- アイテムごとに個別のイベントハンドラを実装する必要があります。
- アイテムが存在しない場所でのマウスプレスを検出するには、シーン側の処理も必要になる場合があります。
- 利点
- 特定のアイテムの動作をカプセル化できます。
- 複数のアイテムが異なるマウスプレス時の動作を持つ場合に便利です。
- シーン全体のイベントハンドラが複雑になるのを防ぎます。
例
#include <QApplication>
#include <QGraphicsScene>
#include <QGraphicsView>
#include <QGraphicsRectItem>
#include <QMouseEvent>
#include <QDebug>
class MyRectItem : public QGraphicsRectItem {
public:
MyRectItem(qreal x, qreal y, qreal width, qreal height, QGraphicsItem *parent = nullptr)
: QGraphicsRectItem(x, y, width, height, parent)
{
setAcceptMouseEvents(true); // マウスイベントを受け付けるように設定
}
protected:
void mousePressEvent(QGraphicsSceneMouseEvent *event) override {
qDebug() << "矩形アイテムがクリックされました (アイテム座標):" << event->pos();
if (event->button() == Qt::LeftButton) {
// 左クリック時の処理
setBrush(Qt::red);
} else if (event->button() == Qt::RightButton) {
// 右クリック時の処理
setBrush(Qt::blue);
}
// アイテムがイベントを処理したことを通知
event->accept();
// 親クラスの mousePressEvent() は通常呼び出しません。
// 必要であれば呼び出すこともできますが、イベントの伝播に注意が必要です。
}
};
int main(int argc, char *argv[]) {
QApplication a(argc, argv);
QGraphicsScene scene;
MyRectItem *rect1 = new MyRectItem(0, 0, 50, 50);
MyRectItem *rect2 = new MyRectItem(60, 0, 50, 50);
scene.addItem(rect1);
scene.addItem(rect2);
QGraphicsView view(&scene);
view.show();
return a.exec();
}
この例では、MyRectItem
クラスが mousePressEvent()
をオーバーライドし、自身がクリックされた際の処理を実装しています。setAcceptMouseEvents(true)
を呼び出すことで、アイテムがマウスイベントを受け付けるようになります。
シグナルとスロットを使用する(間接的な方法)
直接的なイベントハンドラではないですが、アイテムの状態変化やカスタムなイベントをシグナルとして発行し、それをシーンや他のオブジェクトのスロットで処理することで、マウスプレスに関連する動作を実現できます。
- 欠点
- 直接的なイベントハンドラに比べて、イベントの流れがやや複雑になる場合があります。
- マウスプレスの直接的な情報(座標、ボタンなど)を取得するには、何らかの形でスロットに渡す必要があります。
- 利点
- イベントの発生元と処理を分離でき、疎結合な設計になります。
- カスタムなイベントや状態変化に基づいて柔軟な処理が可能です。
例(概念的なもの)
#include <QApplication>
#include <QGraphicsScene>
#include <QGraphicsView>
#include <QGraphicsRectItem>
#include <QMouseEvent>
#include <QObject>
#include <QDebug>
class ClickableRectItem : public QGraphicsRectItem {
Q_OBJECT
public:
ClickableRectItem(qreal x, qreal y, qreal width, qreal height, QGraphicsItem *parent = nullptr)
: QGraphicsRectItem(x, y, width, height, parent)
{
setAcceptMouseEvents(true);
}
signals:
void itemClicked(QPointF scenePos, Qt::MouseButton button);
protected:
void mousePressEvent(QGraphicsSceneMouseEvent *event) override {
emit itemClicked(event->scenePos(), event->button());
QGraphicsRectItem::mousePressEvent(event); // 必要に応じて親クラスの処理も呼ぶ
}
};
class MyScene : public QGraphicsScene {
Q_OBJECT
public:
MyScene(QObject *parent = nullptr) : QGraphicsScene(parent) {}
public slots:
void handleItemClick(QPointF pos, Qt::MouseButton button) {
qDebug() << "シーンがアイテムクリックを捕捉しました:" << pos << ", ボタン:" << button;
// ここでシーン全体の処理を行う
}
};
int main(int argc, char *argv[]) {
QApplication a(argc, argv);
MyScene scene;
ClickableRectItem *rect = new ClickableRectItem(0, 0, 50, 50);
scene.addItem(rect);
// アイテムのシグナルをシーンのスロットに接続
QObject::connect(rect, &ClickableRectItem::itemClicked,
&scene, &MyScene::handleItemClick);
QGraphicsView view(&scene);
view.show();
return a.exec();
}
#include "main.moc" // moc で生成されたファイルを含める
この例では、ClickableRectItem
がクリックされたときに itemClicked
シグナルを発行し、MyScene
の handleItemClick
スロットがそのシグナルを受け取って処理を行います。
イベントフィルタを使用する
QObject::installEventFilter()
を使用すると、特定のオブジェクト(シーンやアイテムなど)に送られるイベントを横取りして処理することができます。
- 欠点
- イベントの流れが把握しにくくなる可能性があります。
- フィルタ関数内で全てのイベントをチェックする必要があるため、パフォーマンスに影響を与える可能性があります。
- 利点
- 既存のクラスを修正せずにイベント処理を追加できます。
- 複数のオブジェクトに対するイベントを集中管理できます。
例(シーンにイベントフィルタをインストールする)
#include <QApplication>
#include <QGraphicsScene>
#include <QGraphicsView>
#include <QGraphicsRectItem>
#include <QMouseEvent>
#include <QEvent>
#include <QDebug>
class MyScene : public QGraphicsScene {
public:
MyScene(QObject *parent = nullptr) : QGraphicsScene(parent) {}
protected:
bool eventFilter(QObject *watched, QEvent *event) override {
if (event->type() == QEvent::GraphicsSceneMousePress) {
QGraphicsSceneMouseEvent *mouseEvent = static_cast<QGraphicsSceneMouseEvent*>(event);
qDebug() << "シーンのイベントフィルタがマウスプレスを捕捉しました:" << mouseEvent->scenePos();
// ここで独自の処理を行う
// 必要に応じて true を返してイベントをブロックしたり、false を返して伝播させたりする
}
// 他のイベントは通常通り処理させる
return QGraphicsScene::eventFilter(watched, event);
}
};
int main(int argc, char *argv[]) {
QApplication a(argc, argv);
MyScene scene;
scene.addItem(new QGraphicsRectItem(0, 0, 50, 50));
// シーン自身にイベントフィルタをインストール
a.installEventFilter(&scene);
QGraphicsView view(&scene);
view.show();
return a.exec();
}
この例では、MyScene
クラスの eventFilter()
関数が、シーンに送られる QEvent::GraphicsSceneMousePress
イベントを捕捉し、処理を行います。
- シーン全体のマウスプレスイベントに対して基本的な処理を行いたい場合
QGraphicsScene::mousePressEvent()
のオーバーライドがシンプルです。 - 既存のクラスの動作を変更せずに、シーン全体または複数のアイテムに対するマウスプレスイベントを監視・処理したい場合
イベントフィルタが役立ちます。 - アイテムとシーンの間で疎結合な連携を実現したい場合
シグナルとスロットの使用が有効です。 - 特定のアイテムの動作を定義する場合
QGraphicsItem::mousePressEvent()
のオーバーライドが適しています。