Qt QGraphicsView::mapToScene()徹底解説:ビュー座標とシーン座標の変換

2025-05-27

QGraphicsView::mapToScene() とは

QGraphicsView::mapToScene() は、QtのGraphics View Frameworkにおいて、ビュー座標系(QGraphicsViewのウィジェット上の座標)をシーン座標系(QGraphicsSceneの座標)に変換するための関数です。

QtのGraphics View Frameworkは、以下の3つの主要なクラスで構成されています。

  1. QGraphicsScene: グラフィックスアイテム(図形、画像、テキストなど)を管理する論理的なキャンバスです。すべてのアイテムはシーン座標系に配置されます。
  2. QGraphicsView: QGraphicsSceneの内容を表示するためのウィジェットです。シーン全体の一部を表示したり、ズームやスクロールなどの操作を提供します。
  3. QGraphicsItem: シーンに配置される個々のグラフィックス要素です。

QGraphicsViewは、表示するQGraphicsSceneに対して、ズーム、回転、平行移動などの変換(Transform)を適用できます。そのため、QGraphicsViewのウィジェット上の特定の点(例えば、マウスカーソルの位置)が、QGraphicsScene上ではどの位置に対応するのかを知る必要がある場合があります。

mapToScene()は、まさにこの変換を行います。

使い方

mapToScene()は、引数としてビュー座標系の点(QPointまたはQPointF)を受け取り、対応するシーン座標系の点(QPointF)を返します。

  • プログラム的にアイテムを配置する
    ビュー上の特定のピクセル位置にアイテムを配置したい場合、そのピクセル位置をmapToScene()でシーン座標に変換し、その変換された座標をアイテムのsetPos()などに渡します。

  • ビューの中心をシーン上の特定の位置に合わせる
    centerOn()関数が提供されていますが、より柔軟な制御が必要な場合にmapToScene()を使って計算することもあります。

  • マウスイベントの処理
    QGraphicsView上でマウスイベントが発生した場合、event->pos()で取得できるのはビュー座標です。このビュー座標をmapToScene()を使ってシーン座標に変換することで、マウスがシーン上のどのアイテムに触れているか、あるいはシーンのどの位置をクリックしたかを判断できます。

    void MyGraphicsView::mousePressEvent(QMouseEvent *event)
    {
        // ビュー座標を取得
        QPoint viewPos = event->pos();
    
        // ビュー座標をシーン座標に変換
        QPointF scenePos = mapToScene(viewPos);
    
        // scenePos を使って、シーン上のアイテムを操作したり、新しいアイテムを追加したりする
        qDebug() << "View Position:" << viewPos;
        qDebug() << "Scene Position:" << scenePos;
    
        QGraphicsView::mousePressEvent(event); // 親クラスのイベントハンドラを呼び出す
    }
    

注意点

  • QGraphicsViewが表示される前(ウィジェットのレイアウトが確定する前)にmapToScene()を呼び出すと、期待する結果が得られない場合があります。これは、スクロールバーの範囲などが正しく設定されていないためです。通常は、show()が呼ばれた後、またはイベントハンドラ内で使用することが推奨されます。
  • 返される座標は常にQPointF(浮動小数点数)です。これは、ズームなどによってピクセル単位よりも細かい精度が必要となるためです。
  • mapToScene()は、QGraphicsViewに適用されている現在の変換(ズーム、回転、スクロールなど)を考慮して変換を行います。
  • QGraphicsItem::mapFromScene(): シーン座標系からグラフィックスアイテムのローカル座標系に変換します。
  • QGraphicsItem::mapToScene(): グラフィックスアイテムのローカル座標系からシーン座標系に変換します。
  • QGraphicsView::mapFromScene(): mapToScene()の逆で、シーン座標系からビュー座標系に変換します。


mapToScene() が常に (0,0) を返す、または予期せぬオフセットが発生する

原因

  • QGraphicsScene::setSceneRect() が設定されていない、または適切でない場合
    QGraphicsSceneの論理的な境界が定義されていないと、QGraphicsViewがシーンの表示範囲を正しく計算できず、mapToScene()の結果に影響を与えることがあります。
  • イベントの発生源が間違っている場合
    QGraphicsView 上のマウスイベントなどで座標を取得する際、QMouseEvent::pos() が親ウィジェット(例えば QMainWindow)からの相対座標を返している場合があります。mapToScene() は、そのQGraphicsView自身のビューポート(表示領域)内の座標を期待します。
  • QGraphicsView がまだ表示されていない(ウィジェットが作成されていない)場合
    QGraphicsView がレンダリングされる前に mapToScene() を呼び出すと、内部の変換行列やスクロールバーの状態が初期化されていないため、正しく変換できません。

トラブルシューティング

  • QGraphicsScene::setSceneRect() を設定する
    明示的にシーンの矩形を設定することで、QGraphicsViewがシーンの大きさを正しく把握し、スクロールバーの範囲なども適切に設定されます。

    QGraphicsScene *scene = new QGraphicsScene(this);
    scene->setSceneRect(0, 0, 1000, 800); // 例: シーンの左上を(0,0)とし、幅1000、高さ800
    QGraphicsView *view = new QGraphicsView(scene);
    
  • 正しい座標を取得する

    • QGraphicsViewのサブクラスでマウスイベントを処理する場合、QMouseEvent::pos() はビューポート内の正しい座標を返します。

    • もし、別のウィジェット(例: QMainWindow)でマウスイベントを処理しているが、そのイベントがQGraphicsView上で発生したものである場合、QWidget::mapTo()QGraphicsView::viewport()->mapFromGlobal(QCursor::pos()) などを使って、まずそのQGraphicsViewのビューポート座標に変換する必要があります。

      // MyMainWindow::mousePressEvent(QMouseEvent *event) のような場合
      // event->pos() は MainWindow の座標
      // graphicsView は MainWindow の子ウィジェットとして追加された QGraphicsView のインスタンス
      QPoint globalPos = event->globalPos(); // スクリーン上の絶対座標
      QPoint viewPortPos = graphicsView->viewport()->mapFromGlobal(globalPos); // ビューポート内の相対座標
      
      QPointF scenePos = graphicsView->mapToScene(viewPortPos);
      qDebug() << "Scene Position:" << scenePos;
      
  • QGraphicsView が表示されてから呼び出す
    QGraphicsView は、通常 show() が呼ばれた後、または関連するイベント(paintEventresizeEvent など)内で正しい変換を提供します。もし、アプリケーションの起動時にすぐにシーン座標が必要な場合は、遅延実行(例: QTimer::singleShot)や、QGraphicsViewshowEvent をオーバーライドしてその中で処理することを検討してください。

座標がずれる(小数点以下の精度問題ではない)

原因

  • スクロールバーの影響
    QGraphicsViewの内部にはスクロールバー(表示されていなくても)の概念があります。ビューポートの開始位置がずれていると、mapToScene()の結果に影響が出ることがあります。通常はQGraphicsViewが自動的に調整しますが、稀に手動での調整が必要な場合があります。
  • 複数の変換が適用されている
    QGraphicsViewに加えて、QGraphicsItem自体にも変換(setTransform()setPos())が適用されている場合、意図しないオフセットやスケーリングが発生することがあります。mapToScene()QGraphicsViewに適用されている変換を考慮しますが、アイテム自身の変換は考慮しません。アイテムのローカル座標からシーン座標への変換には QGraphicsItem::mapToScene() を使用してください。

トラブルシューティング

  • QGraphicsView::setTransform() や QGraphicsView::scale() の使用を再確認する
    意図した変換が適用されているか、特に複数の変換を組み合わせている場合に、それぞれの変換が正しく適用されているかを確認します。変換の順番も重要です。
  • QGraphicsViewのalignment設定を確認する
    QGraphicsView::setAlignment() は、シーン全体がビューポートに収まる場合にシーンがどのように配置されるかを決定します。これが意図しない位置に設定されていると、見た目とmapToScene()の結果が一致しないように感じることがあります。デフォルトはQt::AlignCenterです。必要に応じてQt::AlignTop | Qt::AlignLeftなどに変更することも検討してください。
  • どの座標系からどの座標系へ変換したいかを明確にする
    • ビューポート上のピクセル座標 -> シーン上の論理座標
      QGraphicsView::mapToScene(QPoint viewPoint)
    • アイテム内のローカル座標 -> シーン上の論理座標
      QGraphicsItem::mapToScene(QPointF itemPoint)

アイテムをシーンに追加したのに表示されない、またはクリックしても反応しない

原因

  • QGraphicsView::setInteractive(false) になっている
    イベントが伝播しない。
  • アイテムのZ値が低い
    他のアイテムの下に隠れている。
  • QGraphicsScene::setSceneRect() が小さすぎる
    シーンの矩形が小さく、アイテムがその外に配置されているため、QGraphicsViewが表示できない。
  • mapToScene() の直接的なエラーではないが、関連する問題
    mapToScene() の結果を使ってアイテムを配置しようとした際に、その座標がシーンの有効な範囲外になっている可能性があります。

トラブルシューティング

  • QGraphicsView::setInteractive(true) を設定する
    マウスイベントを有効にします。
  • QGraphicsItem::setZValue() で表示順序を調整する
    手前に表示したいアイテムには大きなZ値を設定します。
  • QGraphicsScene::setSceneRect() を十分に大きく設定する
    シーンのアイテムの最も外側の境界を含むように setSceneRect を設定するか、または QGraphicsScene::itemsBoundingRect() を使用して、シーンのアイテム全体の境界を取得し、それに基づいてビューを調整します。
  • QGraphicsScene::items() や QGraphicsScene::itemAt() でアイテムの存在を確認する
    デバッグ時にこれらの関数を使って、アイテムが期待するシーン座標に存在するかを確認できます。

高DPI環境でのスケーリング問題

原因

  • 高DPI環境では、物理ピクセルと論理ピクセル(デバイス非依存ピクセル)の間にスケーリング係数が存在します。Qtは通常これを自動的に処理しますが、明示的なピクセル操作を行う場合に問題が発生することがあります。

トラブルシューティング

  • カスタムの描画や手動でピクセルを扱う際には、デバイスピクセルレシオを考慮する必要があるかもしれません。しかし、QGraphicsViewのフレームワークは、ほとんどの場合、これらの詳細を内部で処理します。
  • Qt 5.6以降では、デフォルトで高DPIスケーリングが有効になっています。特別な設定が不要な場合が多いですが、もし問題が発生する場合は、アプリケーション起動時にQt::AA_EnableHighDpiScalingアトリビュートを設定してみることを検討してください。
    QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
    QApplication a(argc, argv);
    


例1: マウスクリック位置に円を追加する

この例では、QGraphicsView を継承したカスタムクラスを作成し、マウスがクリックされたビュー座標をシーン座標に変換して、その位置に小さな円を追加します。

MyGraphicsView.h

#ifndef MYGRAPHICSVIEW_H
#define MYGRAPHICSVIEW_H

#include <QGraphicsView>
#include <QGraphicsScene>
#include <QMouseEvent>
#include <QDebug>
#include <QGraphicsEllipseItem>

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

protected:
    // マウスプレスイベントをオーバーライド
    void mousePressEvent(QMouseEvent *event) override;

private:
    QGraphicsScene *m_scene; // シーンへのポインタ
};

#endif // MYGRAPHICSVIEW_H

MyGraphicsView.cpp

#include "MyGraphicsView.h"

MyGraphicsView::MyGraphicsView(QWidget *parent)
    : QGraphicsView(parent)
{
    // シーンを作成し、ビューに設定
    m_scene = new QGraphicsScene(this);
    setScene(m_scene);

    // シーンの論理的な範囲を設定(任意だが推奨)
    m_scene->setSceneRect(-500, -500, 1000, 1000);

    // ユーザーインタラクションを有効にする
    setInteractive(true);

    // デバッグ用に背景色を設定
    m_scene->setBackgroundBrush(Qt::lightGray);
}

void MyGraphicsView::mousePressEvent(QMouseEvent *event)
{
    // マウスの左ボタンが押された場合のみ処理
    if (event->button() == Qt::LeftButton) {
        // 1. ビュー座標(ウィジェット上のピクセル座標)を取得
        QPoint viewPos = event->pos();
        qDebug() << "ビュー座標 (View Pos):" << viewPos;

        // 2. mapToScene() を使ってビュー座標をシーン座標に変換
        QPointF scenePos = mapToScene(viewPos);
        qDebug() << "シーン座標 (Scene Pos):" << scenePos;

        // 3. 変換されたシーン座標に小さな円を追加
        qreal radius = 5.0;
        QGraphicsEllipseItem *circle = new QGraphicsEllipseItem(
            scenePos.x() - radius, scenePos.y() - radius, // 左上点の計算
            radius * 2, radius * 2
        );
        circle->setBrush(Qt::blue);
        m_scene->addItem(circle);
    }

    // 親クラスのイベントハンドラを呼び出す(これにより、デフォルトのビューの動作が維持される)
    QGraphicsView::mousePressEvent(event);
}

main.cpp

#include <QApplication>
#include <QMainWindow>
#include <QVBoxLayout>
#include "MyGraphicsView.h"

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

    QMainWindow window;
    window.setWindowTitle("QGraphicsView::mapToScene() Example");
    window.resize(800, 600);

    MyGraphicsView *graphicsView = new MyGraphicsView(&window);

    // QGraphicsView をメインウィンドウのセントラルウィジェットとして設定
    // またはレイアウトに追加
    QWidget *centralWidget = new QWidget(&window);
    QVBoxLayout *layout = new QVBoxLayout(centralWidget);
    layout->addWidget(graphicsView);
    window.setCentralWidget(centralWidget);

    window.show();

    return a.exec();
}

解説

  1. MyGraphicsView クラスで mousePressEvent をオーバーライドしています。
  2. event->pos() で、MyGraphicsView ウィジェット内でのマウスのクリック位置(ビュー座標)を取得します。
  3. mapToScene(viewPos) を呼び出すことで、このビュー座標が現在のビューのズームやスクロールの状態を考慮して、対応するQGraphicsScene上の座標 (QPointF) に変換されます。
  4. 変換された scenePos を使用して、QGraphicsEllipseItem をシーンに追加します。

このコードを実行すると、QGraphicsView 内をクリックするたびに、そのクリック位置に対応するシーン座標に青い円が描画されます。ビューをズームイン・ズームアウトしても、円は常にシーン上の同じ論理的な位置に描画されることを確認できます。

例2: マウスドラッグで矩形を描画する

この例では、マウスドラッグの開始点と終了点を mapToScene() で取得し、その2点を使ってシーン上に矩形を描画します。

MyGraphicsView.h (例1と同じMyGraphicsViewクラスに以下を追加)

#ifndef MYGRAPHICSVIEW_H
#define MYGRAPHICSVIEW_H

#include <QGraphicsView>
#include <QGraphicsScene>
#include <QMouseEvent>
#include <QDebug>
#include <QGraphicsEllipseItem>
#include <QGraphicsRectItem> // 追加
#include <QPointF>           // 追加

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

protected:
    void mousePressEvent(QMouseEvent *event) override;
    void mouseMoveEvent(QMouseEvent *event) override;   // 追加
    void mouseReleaseEvent(QMouseEvent *event) override; // 追加

private:
    QGraphicsScene *m_scene;
    QPointF m_startScenePos; // ドラッグ開始時のシーン座標
    QGraphicsRectItem *m_currentRectItem; // 現在描画中の矩形アイテム
};

#endif // MYGRAPHICSVIEW_H

MyGraphicsView.cpp (例1のMyGraphicsView::mousePressEvent()を修正し、mouseMoveEvent()とmouseReleaseEvent()を追加)

#include "MyGraphicsView.h"

MyGraphicsView::MyGraphicsView(QWidget *parent)
    : QGraphicsView(parent)
    , m_currentRectItem(nullptr) // 初期化
{
    m_scene = new QGraphicsScene(this);
    setScene(m_scene);
    m_scene->setSceneRect(-500, -500, 1000, 1000);
    setInteractive(true);
    m_scene->setBackgroundBrush(Qt::lightGray);

    // マウスイベントのトラッキングを有効にする (mouseMoveEvent が常に発生するように)
    setMouseTracking(true);
}

void MyGraphicsView::mousePressEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton) {
        // ドラッグ開始時のシーン座標を保存
        m_startScenePos = mapToScene(event->pos());
        qDebug() << "ドラッグ開始 シーン座標:" << m_startScenePos;

        // 新しい矩形アイテムを作成し、シーンに追加
        // 初期状態では幅と高さが0の矩形
        m_currentRectItem = new QGraphicsRectItem();
        m_currentRectItem->setPen(QPen(Qt::red, 2)); // 赤い線で描画
        m_scene->addItem(m_currentRectItem);
    }

    QGraphicsView::mousePressEvent(event);
}

void MyGraphicsView::mouseMoveEvent(QMouseEvent *event)
{
    // 左ボタンが押されている間のみ処理
    if (event->buttons() & Qt::LeftButton && m_currentRectItem) {
        // 現在のマウス位置をシーン座標に変換
        QPointF currentScenePos = mapToScene(event->pos());

        // ドラッグ開始点と現在点から矩形を計算
        QRectF rect;
        rect.setTopLeft(QPointF(qMin(m_startScenePos.x(), currentScenePos.x()),
                                qMin(m_startScenePos.y(), currentScenePos.y())));
        rect.setBottomRight(QPointF(qMax(m_startScenePos.x(), currentScenePos.x()),
                                    qMax(m_startScenePos.y(), currentScenePos.y())));

        // 矩形アイテムの形状を更新
        m_currentRectItem->setRect(rect);
    }
    
    QGraphicsView::mouseMoveEvent(event);
}

void MyGraphicsView::mouseReleaseEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton) {
        // ドラッグ終了時のシーン座標を取得
        QPointF endScenePos = mapToScene(event->pos());
        qDebug() << "ドラッグ終了 シーン座標:" << endScenePos;

        // 最終的な矩形を設定
        if (m_currentRectItem) {
            QRectF finalRect;
            finalRect.setTopLeft(QPointF(qMin(m_startScenePos.x(), endScenePos.x()),
                                         qMin(m_startScenePos.y(), endScenePos.y())));
            finalRect.setBottomRight(QPointF(qMax(m_startScenePos.x(), endScenePos.x()),
                                             qMax(m_startScenePos.y(), endScenePos.y())));
            m_currentRectItem->setRect(finalRect);
            m_currentRectItem = nullptr; // 次のドラッグのためにリセット
        }
    }

    QGraphicsView::mouseReleaseEvent(event);
}

main.cpp (例1と同じ)

#include <QApplication>
#include <QMainWindow>
#include <QVBoxLayout>
#include "MyGraphicsView.h"

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

    QMainWindow window;
    window.setWindowTitle("QGraphicsView::mapToScene() Drag Draw Rect Example");
    window.resize(800, 600);

    MyGraphicsView *graphicsView = new MyGraphicsView(&window);

    QWidget *centralWidget = new QWidget(&window);
    QVBoxLayout *layout = new QVBoxLayout(centralWidget);
    layout->addWidget(graphicsView);
    window.setCentralWidget(centralWidget);

    window.show();

    return a.exec();
}

解説

  1. mousePressEvent でドラッグの開始点 (m_startScenePos) をシーン座標で記録し、仮の QGraphicsRectItem (m_currentRectItem) を作成します。
  2. mouseMoveEvent では、左ボタンが押されている間、現在のマウス位置を mapToScene() でシーン座標に変換し、m_startScenePos との間の矩形を計算して m_currentRectItem の形状を動的に更新します。これにより、ドラッグ中に矩形が「伸びる」ように見えます。
  3. mouseReleaseEvent でドラッグが終了した際の最終的なマウス位置を mapToScene() でシーン座標に変換し、m_currentRectItem の最終的な形状を確定します。

この例では、mapToScene() を使ってビューの変換(ズーム、スクロールなど)に関わらず、マウスのドラッグ操作がシーン上の正確な矩形描画に結びつくことを示しています。



QGraphicsView::itemAt() を直接使用する

これは mapToScene()代替というよりは、一般的なユースケースにおけるより直接的な解決策と言えます。

目的
特定のビュー座標にあるアイテムを直接取得したい場合。

説明
QGraphicsView::itemAt(const QPoint &pos) は、ビュー座標 pos にある一番手前の QGraphicsItem を返します。内部的には mapToScene() を使用してビュー座標をシーン座標に変換し、その後 QGraphicsScene::itemAt() を呼び出してアイテムを検索します。

メリット

  • コードが簡潔になります。
  • mapToScene() を自分で呼び出して、その結果を QGraphicsScene::itemAt() に渡す手間が省けます。

デメリット

  • その座標にあるアイテムそのものが欲しい場合にのみ有効です。クリック位置に新しいアイテムを追加するなどの場合は、mapToScene() を使う必要があります。

コード例

void MyGraphicsView::mousePressEvent(QMouseEvent *event)
{
    // ビュー座標にあるアイテムを直接取得
    QGraphicsItem *item = itemAt(event->pos());

    if (item) {
        qDebug() << "クリックされたアイテム:" << item;
        // アイテムの操作(例: 選択状態の切り替え)
        item->setSelected(!item->isSelected());
    } else {
        qDebug() << "アイテムがクリックされませんでした。";
    }

    QGraphicsView::mousePressEvent(event);
}

QGraphicsScene::itemAt() と QGraphicsView::mapToScene() を組み合わせる

これは itemAt() の内部動作に近いです。グローバル座標や、QGraphicsView の外部のウィジェットから取得した座標をシーン座標に変換してアイテムを検索する場合に利用します。

目的
QGraphicsView 以外の場所で取得した座標をシーン上のアイテムにマッピングしたい場合。

説明
例えば、QMainWindow のイベントでマウスのグローバル座標を取得し、それを特定の QGraphicsView 内のシーン座標に変換してアイテムを検索する、といったシナリオです。

コード例

// 例えば、QMainWindow のサブクラスのイベントハンドラ
void MyMainWindow::mousePressEvent(QMouseEvent *event)
{
    // グローバル座標
    QPoint globalPos = event->globalPos();

    // QGraphicsView のビューポート内の座標に変換
    // graphicsView は MyGraphicsView のインスタンスと仮定
    QPoint viewPortPos = graphicsView->viewport()->mapFromGlobal(globalPos);

    // ビューポート座標が QGraphicsView の範囲内にあるか確認
    if (graphicsView->viewport()->rect().contains(viewPortPos)) {
        // ビューポート座標をシーン座標に変換
        QPointF scenePos = graphicsView->mapToScene(viewPortPos);

        // シーン座標にあるアイテムを取得
        QGraphicsItem *item = graphicsView->scene()->itemAt(scenePos, QTransform()); // QTransform() は変換なしを意味
        if (item) {
            qDebug() << "グローバルクリックでアイテム:" << item;
            item->setSelected(!item->isSelected());
        } else {
            qDebug() << "グローバルクリックでアイテムなし。";
        }
    }

    QMainWindow::mousePressEvent(event);
}

QGraphicsItem::mapToScene() を使用する(アイテムのローカル座標からシーン座標へ)

QGraphicsView::mapToScene() はビュー座標からシーン座標への変換ですが、QGraphicsItem::mapToScene()アイテムのローカル座標からシーン座標への変換です。これは全く異なる目的を持ちますが、座標変換という点で関連します。