このページでは

ドラッグ&ドロップロボットの例

グラフィックスビュー内でアイテムをドラッグ&ドロップする方法を示します。

「ドラッグ&ドロップロボット」のサンプルでは、QGraphicsItem のサブクラスでドラッグ&ドロップを実装する方法と、Qtのアニメーションフレームワークを使用してアイテムをアニメーション化する方法について示しています。

ドラッグ&ドロップ操作に応答するロボットの応用

Graphics View は、QGraphicsItem クラスを継承した多数のカスタム 2D グラフィック アイテムを管理および操作するための `QGraphicsScene ` クラスと、ズームや回転をサポートしてアイテムを視覚化する `QGraphicsView ` ウィジェットを提供します。

この例は、Robot クラス、ColorItem クラス、およびmain関数で構成されています。Robot クラスは、RobotHead やRobotLimb など、RobotPart を継承した複数の肢からなる単純なロボットを表しています。ColorItem クラスは、ドラッグ可能な色付きの楕円を提供し、main() 関数はメインアプリケーションウィンドウを提供します。

まず、Robot クラスを確認し、QPropertyAnimation を使用して各パーツを個別に回転させたりアニメーションさせたりできるように、それらをどのように組み立てるかを見ていきます。次に、ColorItem クラスを確認し、アイテム間のドラッグ&ドロップを実装する方法を説明します。最後に、main()関数を確認し、すべてのパーツを組み合わせて最終的なアプリケーションを構築する方法を学びます。

Robotクラスの定義

ロボットは、RobotHead 、RobotTorso 、および上腕・下腕・脚に使用されるRobotLimb という3つの主要なクラスで構成されています。すべてのパーツはRobotPart クラスを継承しており、このクラスはQGraphicsObject を継承しています。Robot クラス自体には視覚的な外観はなく、ロボットのルートノードとしてのみ機能します。

まずは、RobotPart クラスの宣言から見ていきましょう。

class RobotPart : public QGraphicsObject
{
public:
    RobotPart(QGraphicsItem *parent = nullptr);

protected:
    void dragEnterEvent(QGraphicsSceneDragDropEvent *event) override;
    void dragLeaveEvent(QGraphicsSceneDragDropEvent *event) override;
    void dropEvent(QGraphicsSceneDragDropEvent *event) override;

    QColor color = Qt::lightGray;
    bool dragOver = false;
};

この基底クラスは `QGraphicsObject` を継承しています。`QGraphicsObject ` は `QObject` を継承することでシグナルとスロットを提供し、さらに `Q_PROPERTY` を使用して `QGraphicsItem` のプロパティを宣言しています。これにより、`QPropertyAnimation` からこれらのプロパティにアクセスできるようになります。

また、RobotPartは、ドロップイベントを受け付けるための3つの最も重要なイベントハンドラ、dragEnterEvent()、dragLeaveEvent()、およびdropEvent()も実装しています。

色はメンバー変数として格納され、dragOver 変数も同様に格納されます。この変数は、後で、その肢にドラッグされてきた色を受け入れられることを視覚的に示すために使用します。

RobotPart::RobotPart(QGraphicsItem *parent)
    : QGraphicsObject(parent), color(Qt::lightGray)
{
    setAcceptDrops(true);
}

RobotPartのコンストラクタは、dragOverメンバを初期化し、色をQt::lightGray に設定します。コンストラクタ本体では、setAcceptDrops(true)を呼び出すことで、ドロップイベントを受け入れるためのサポートを有効にします。

このクラスの残りの実装は、ドラッグ&ドロップをサポートするためのものです。

void RobotPart::dragEnterEvent(QGraphicsSceneDragDropEvent *event)
{
    if (event->mimeData()->hasColor()) {
        event->setAccepted(true);
        dragOver = true;
        update();
    } else {
        event->setAccepted(false);
    }
}

dragEnterEvent() ハンドラは、ドラッグ&ドロップ要素がロボットパーツの領域内にドラッグされた際に呼び出されます。

このハンドラの実装では、このアイテム全体として、受信したドラッグオブジェクトに関連付けられた MIME データを受け入れることができるかどうかを判断します。RobotPart は、色のドロップを受け入れるすべてのパーツに対する基本動作を提供します。 したがって、受け取ったドラッグオブジェクトに色が含まれている場合、イベントは受け入れられ、dragOver をtrue に設定し、update()を呼び出してユーザーに視覚的なフィードバックを提供します。そうでない場合はイベントは無視され、その結果、イベントは親要素へと伝播します。

void RobotPart::dragLeaveEvent(QGraphicsSceneDragDropEvent *event)
{
    Q_UNUSED(event);
    dragOver = false;
    update();
}

dragLeaveEvent() ハンドラは、ドラッグ&ドロップ要素がロボットパーツの領域からドラッグされて離れたときに呼び出されます。私たちの実装では、単にdragOver をfalse にリセットし、update() を呼び出して、ドラッグがこのアイテムから離れたことを示す視覚的なフィードバックを提供しています。

void RobotPart::dropEvent(QGraphicsSceneDragDropEvent *event)
{
    dragOver = false;
    if (event->mimeData()->hasColor())
        color = qvariant_cast<QColor>(event->mimeData()->colorData());
    update();
}

dropEvent() ハンドラは、ドラッグ&ドロップ要素がアイテム上にドロップされたとき(つまり、ドラッグ中にアイテム上でマウスボタンが離されたとき)に呼び出されます。

ここで、dragOver をfalseにリセットし、アイテムの新しい色を割り当て、update()を呼び出します。

RobotHead 、RobotTorso 、およびRobotLimb の宣言と実装は実質的に同一です。RobotHead には1つだけ小さな違いがあるため、これについては詳細に検討し、他のクラスについては読者の演習課題とします。

class RobotHead : public RobotPart
{
public:
    RobotHead(QGraphicsItem *parent = nullptr);

    QRectF boundingRect() const override;
    void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = nullptr) override;

protected:
    void dragEnterEvent(QGraphicsSceneDragDropEvent *event) override;
    void dropEvent(QGraphicsSceneDragDropEvent *event) override;

private:
    QPixmap pixmap;
};

RobotHead クラスはRobotPart を継承し、boundingRect()およびpaint()の必要な実装を提供します。また、dragEnterEvent()およびdropEvent()を再実装して、画像のドロップに対する特別な処理を提供します。

このクラスには、画像のドロップを受け付ける機能を実装するために使用できる、プライベートなpixmapメンバが含まれています。

RobotHead::RobotHead(QGraphicsItem *parent)
    : RobotPart(parent)
{
}

RobotHead このクラスのコンストラクタは比較的単純で、単にRobotPart のコンストラクタを呼び出すだけです。

QRectF RobotHead::boundingRect() const
{
    return QRectF(-15, -50, 30, 50);
}

boundingRect() の再実装は、ヘッドのエクステントを返します。回転の中心をアイテムの下部中央にしたいので、(-15, -50) から始まり、幅 30 単位、高さ 50 単位に広がるバウンディング矩形を選択しました。 頭を回転させると、「首」は静止したまま、頭頂部が左右に傾きます。

void RobotHead::paint(QPainter *painter,
           const QStyleOptionGraphicsItem *option, QWidget *widget)
{
    Q_UNUSED(option);
    Q_UNUSED(widget);
    if (pixmap.isNull()) {
        painter->setBrush(dragOver ? color.lighter(130) : color);
        painter->drawRoundedRect(-10, -30, 20, 30, 25, 25, Qt::RelativeSize);
        painter->setBrush(Qt::white);
        painter->drawEllipse(-7, -3 - 20, 7, 7);
        painter->drawEllipse(0, -3 - 20, 7, 7);
        painter->setBrush(Qt::black);
        painter->drawEllipse(-5, -1 - 20, 2, 2);
        painter->drawEllipse(2, -1 - 20, 2, 2);
        painter->setPen(QPen(Qt::black, 2));
        painter->setBrush(Qt::NoBrush);
        painter->drawArc(-6, -2 - 20, 12, 15, 190 * 16, 160 * 16);
    } else {
        painter->scale(.2272, .2824);
        painter->drawPixmap(QPointF(-15 * 4.4, -50 * 3.54), pixmap);
    }
}

paint() では、実際の頭部を描画します。実装は 2 つのセクションに分かれています。頭部に画像がドラッグ&ドロップされている場合はその画像を描画し、そうでない場合はシンプルなベクターグラフィックスを用いて丸みを帯びた長方形のロボットの頭部を描画します。

パフォーマンス上の理由から、描画内容の複雑さによっては、一連のベクトル演算を行うよりも、画像として頭部を描画する方が高速な場合がよくあります。

void RobotHead::dragEnterEvent(QGraphicsSceneDragDropEvent *event)
{
    if (event->mimeData()->hasImage()) {
        event->setAccepted(true);
        dragOver = true;
        update();
    } else {
        RobotPart::dragEnterEvent(event);
    }
}

ロボットの頭部は画像のドラッグ&ドロップを受け付けることができます。これをサポートするため、dragEnterEvent() の再実装では、ドラッグオブジェクトに画像データが含まれているかどうかを確認し、含まれている場合はそのイベントを受け入れます。そうでない場合は、基底クラスのRobotPart の実装にフォールバックします。

void RobotHead::dropEvent(QGraphicsSceneDragDropEvent *event)
{
    if (event->mimeData()->hasImage()) {
        dragOver = false;
        pixmap = qvariant_cast<QPixmap>(event->mimeData()->imageData());
        update();
    } else {
        RobotPart::dropEvent(event);
    }
}

画像サポートをさらに進めるには、dropEvent() も実装する必要があります。ドラッグオブジェクトに画像データが含まれているかを確認し、含まれている場合は、このデータをメンバー変数 pixmap として格納し、update() を呼び出します。この pixmap は、先ほど確認したpaint() の実装内で使用されます。

RobotTorso RobotLimb と は類似しているため、ここでは クラスに直接進みましょう。RobotHead Robot

class Robot : public RobotPart
{
public:
    Robot(QGraphicsItem *parent = nullptr);

    QRectF boundingRect() const override;
    void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = nullptr) override;
};

Robot クラスもRobotPart を継承しており、他のパーツと同様にboundingRect()およびpaint()を実装しています。ただし、このクラスにはかなり特殊な実装が施されています:

QRectF Robot::boundingRect() const
{
    return QRectF();
}

void Robot::paint(QPainter *painter,
                  const QStyleOptionGraphicsItem *option, QWidget *widget)
{
    Q_UNUSED(painter);
    Q_UNUSED(option);
    Q_UNUSED(widget);
}

Robot クラスはロボットの残りの部分の基底ノードとしてのみ使用されるため、視覚的な表現を持ちません。したがって、そのboundingRect()の実装ではnullのQRectF を返すことができ、paint()関数は何も行いません。

Robot::Robot(QGraphicsItem *parent)
    : RobotPart(parent)
{
    setFlag(ItemHasNoContents);

    QGraphicsObject *torsoItem = new RobotTorso(this);
    QGraphicsObject *headItem = new RobotHead(torsoItem);
    QGraphicsObject *upperLeftArmItem = new RobotLimb(torsoItem);
    QGraphicsObject *lowerLeftArmItem = new RobotLimb(upperLeftArmItem);
    QGraphicsObject *upperRightArmItem = new RobotLimb(torsoItem);
    QGraphicsObject *lowerRightArmItem = new RobotLimb(upperRightArmItem);
    QGraphicsObject *upperRightLegItem = new RobotLimb(torsoItem);
    QGraphicsObject *lowerRightLegItem = new RobotLimb(upperRightLegItem);
    QGraphicsObject *upperLeftLegItem = new RobotLimb(torsoItem);
    QGraphicsObject *lowerLeftLegItem = new RobotLimb(upperLeftLegItem);

コンストラクタはまず、ItemHasNoContents フラグを設定することから始まります。これは、視覚的な外観を持たないアイテムに対する些細な最適化です。

次に、ロボットのすべてのパーツ(頭部、胴体、上腕・下腕、脚)を構築します。積み重ね順は非常に重要であり、要素が適切に回転・移動するように、親子階層を利用します。 胴体がルート要素であるため、まずこれを生成します。次に頭部を生成し、胴体を `HeadItem` のコンストラクタに渡します。これにより、頭部は胴体の子要素となります。胴体を回転させると、頭部もそれに追従します。このパターンは、残りの手足にも同様に適用されます。

    headItem->setPos(0, -18);
    upperLeftArmItem->setPos(-15, -10);
    lowerLeftArmItem->setPos(30, 0);
    upperRightArmItem->setPos(15, -10);
    lowerRightArmItem->setPos(30, 0);
    upperRightLegItem->setPos(10, 32);
    lowerRightLegItem->setPos(30, 0);
    upperLeftLegItem->setPos(-10, 32);
    lowerLeftLegItem->setPos(30, 0);

ロボットの各パーツは慎重に配置されます。例えば、左上腕は胴体の左上部に、右上腕は右上部に、それぞれ正確に配置されます。

    QParallelAnimationGroup *animation = new QParallelAnimationGroup(this);

    QPropertyAnimation *headAnimation = new QPropertyAnimation(headItem, "rotation");
    headAnimation->setStartValue(20);
    headAnimation->setEndValue(-20);
    QPropertyAnimation *headScaleAnimation = new QPropertyAnimation(headItem, "scale");
    headScaleAnimation->setEndValue(1.1);
    animation->addAnimation(headAnimation);
    animation->addAnimation(headScaleAnimation);

次のセクションでは、すべてのアニメーションオブジェクトを作成します。このスニペットは、ヘッドのスケールと回転を制御する 2 つのアニメーションを示しています。2 つの `QPropertyAnimation ` インスタンスは、オブジェクト、プロパティ、およびそれぞれの開始値と終了値を設定するだけです。

すべてのアニメーションは、1つの最上位の並列アニメーショングループによって制御されます。スケールと回転のアニメーションは、このグループに追加されます。

残りのアニメーションも同様の方法で定義されます。

    for (int i = 0; i < animation->animationCount(); ++i) {
        QPropertyAnimation *anim = qobject_cast<QPropertyAnimation *>(animation->animationAt(i));
        anim->setEasingCurve(QEasingCurve::SineCurve);
        anim->setDuration(2000);
    }

    animation->setLoopCount(-1);
    animation->start();

最後に、各アニメーションにイージングカーブと持続時間を設定し、最上位のアニメーショングループが無限ループになるように設定して、そのアニメーションを開始します。

ColorItem クラスの定義

ColorItem クラスは、押してロボットパーツに色をドラッグできる円形のアイテムを表します。

class ColorItem : public QGraphicsItem
{
public:
    ColorItem();

    QRectF boundingRect() const override;
    void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;

protected:
    void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
    void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override;
    void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override;

private:
    QColor color;
};

このクラスは非常に単純です。アニメーションを使用せず、プロパティやシグナル、スロットも不要であるため、リソースを節約する観点から、`QGraphicsItem `(`QGraphicsObject` ではなく)を継承するのが最も自然です。

このクラスは、必須のboundingRect()およびpaint()関数を宣言し、mousePressEvent()、mouseMoveEvent()、mouseReleaseEvent()の再実装を追加しています。また、1つのプライベートなcolorメンバを含んでいます。

その実装を見てみましょう。

ColorItem::ColorItem()
    : color(QRandomGenerator::global()->bounded(256), QRandomGenerator::global()->bounded(256), QRandomGenerator::global()->bounded(256))
{
    setToolTip(QString("QColor(%1, %2, %3)\n%4")
              .arg(color.red()).arg(color.green()).arg(color.blue())
              .arg("Click and drag this color onto the robot!"));
    setCursor(Qt::OpenHandCursor);
    setAcceptedMouseButtons(Qt::LeftButton);
}

ColorItemのコンストラクタは、QRandomGenerator を利用して、color メンバーに不透明なランダムな色を割り当てます。使いやすさを向上させるため、ユーザーに役立つヒントを提供するツールチップを割り当て、適切なカーソルも設定します。これにより、マウスポインタがアイテムの上にホバーした際に、カーソルがQt::OpenHandCursor に変化するようになります。

最後に、setAcceptedMouseButtons() を呼び出し、このアイテムがQt::LeftButton のみを処理できるようにします。これにより、常にマウスの左ボタンが押されたり離されたりすることのみを想定できるため、マウスイベントハンドラが大幅に簡素化されます。

QRectF ColorItem::boundingRect() const
{
    return QRectF(-15.5, -15.5, 34, 34);
}

アイテムのバウンディング矩形は、アイテムの原点 (0, 0) を中心とする固定の 30×30 単位であり、スケーラブルなペンが輪郭を描画できるように、全方向に 0.5 単位ずつ調整されています。 視覚的な仕上げとして、境界は右下方向に数ユニット分ずらされており、シンプルなドロップシャドウを表示するためのスペースが確保されています。

void ColorItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
    Q_UNUSED(option);
    Q_UNUSED(widget);
    painter->setPen(Qt::NoPen);
    painter->setBrush(Qt::darkGray);
    painter->drawEllipse(-12, -12, 30, 30);
    painter->setPen(QPen(Qt::black, 1));
    painter->setBrush(QBrush(color));
    painter->drawEllipse(-15, -15, 30, 30);
}

paint() の実装では、1 ユニットの黒いアウトライン、単色の塗りつぶし、および濃い灰色のドロップシャドウを持つ楕円を描画します。

void ColorItem::mousePressEvent(QGraphicsSceneMouseEvent *)
{
    setCursor(Qt::ClosedHandCursor);
}

mousePressEvent() ハンドラは、アイテムの領域内でマウスボタンを押すと呼び出されます。この実装では、カーソルをQt::ClosedHandCursor に設定するだけです。

void ColorItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *)
{
    setCursor(Qt::OpenHandCursor);
}

mouseReleaseEvent() ハンドラは、アイテムの領域内でマウスボタンを押した後にそのボタンを離したときに呼び出されます。 この実装では、カーソルをQt::OpenHandCursor に戻します。マウスの押下および離しイベントハンドラを組み合わせることで、ユーザーに有用な視覚的なフィードバックを提供します。CircleItem の上にマウスポインタを移動させると、カーソルが開いた手の形に変わります。アイテムを押すと、閉じた手のカーソルが表示されます。離すと、再び開いた手のカーソルに戻ります。

void ColorItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
    if (QLineF(event->screenPos(), event->buttonDownScreenPos(Qt::LeftButton))
        .length() < QApplication::startDragDistance()) {
        return;
    }

    QDrag *drag = new QDrag(event->widget());
    QMimeData *mime = new QMimeData;
    drag->setMimeData(mime);

mouseMoveEvent() ハンドラは、ColorItem の領域内でマウスボタンを押した後にマウスを動かすと呼び出されます。この実装は、CircleItem にとって最も重要なロジック、すなわちドラッグを開始・管理するコードを提供します。

この実装では、まずマウスが、マウスのジッターノイズを排除できるほど十分に引きずられたかどうかを確認します。マウスがアプリケーションのドラッグ開始距離よりも遠くまで引きずられた場合にのみ、ドラッグを開始するようにします。

続いて、QDrag オブジェクトを作成し、そのコンストラクタにイベントwidget (つまり、QGraphicsView のビューポート)を渡します。Qtは、このオブジェクトが適切なタイミングで削除されるように保証します。また、色や画像データを格納できるQMimeData インスタンスを作成し、これをドラッグオブジェクトに割り当てます。

    static int n = 0;
    if (n++ > 2 && QRandomGenerator::global()->bounded(3) == 0) {
        QImage image(":/images/head.png");
        mime->setImageData(image);

        drag->setPixmap(QPixmap::fromImage(image).scaled(30, 40));
        drag->setHotSpot(QPoint(15, 30));

このコードスニペットの結果は多少ランダムです。時折、特定の画像がドラッグオブジェクトのMIMEデータに割り当てられます。また、ピクマップもドラッグオブジェクトのピクマップとして割り当てられます。これにより、ドラッグされている画像がマウスカーソルの下にピクマップとして表示されるようになります。

    } else {
        mime->setColorData(color);
        mime->setText(QString("#%1%2%3")
                      .arg(color.red(), 2, 16, QLatin1Char('0'))
                      .arg(color.green(), 2, 16, QLatin1Char('0'))
                      .arg(color.blue(), 2, 16, QLatin1Char('0')));

        QPixmap pixmap(34, 34);
        pixmap.fill(Qt::white);

        QPainter painter(&pixmap);
        painter.translate(15, 15);
        painter.setRenderHint(QPainter::Antialiasing);
        paint(&painter, nullptr, nullptr);
        painter.end();

        pixmap.setMask(pixmap.createHeuristicMask());

        drag->setPixmap(pixmap);
        drag->setHotSpot(QPoint(15, 20));
    }

それ以外の場合、つまり最も一般的な結果として、単純な色がドラッグオブジェクトのMIMEデータに割り当てられます。このColorItem を新しいピクマップとして描画することで、色が「ドラッグ」されていることをユーザーに視覚的にフィードバックします。

    drag->exec();
    setCursor(Qt::OpenHandCursor);
}

最後に、ドラッグを実行します。QDrag::exec() はイベントループに再進入し、ドラッグがドロップされたか、キャンセルされた場合にのみ終了します。いずれの場合も、カーソルをQt::OpenHandCursor にリセットします。

main() 関数

Robot クラスとColorItem クラスの実装が完了したので、main() 関数内でこれらをすべて組み合わせていきます。

int main(int argc, char **argv)
{
    QApplication app(argc, argv);

まず、QApplication を生成し、乱数生成器を初期化します。これにより、アプリケーションの起動ごとにカラーアイテムの色が異なるようになります。

    QGraphicsScene scene(-200, -200, 400, 400);

    for (int i = 0; i < 10; ++i) {
        ColorItem *item = new ColorItem;
        item->setPos(::sin((i * 6.28) / 10.0) * 150,
                     ::cos((i * 6.28) / 10.0) * 150);

        scene.addItem(item);
    }

    Robot *robot = new Robot;
    robot->setTransform(QTransform::fromScale(1.2, 1.2), true);
    robot->setPos(0, -20);
    scene.addItem(robot);

固定サイズのシーンを生成し、円状に配置された 10 個の `ColorItem ` インスタンスを作成します。各アイテムをシーンに追加します。

この円の中心に、Robot のインスタンスを1つ作成します。ロボットのスケールを調整し、数ユニット上に移動させます。その後、シーンに追加します。

    GraphicsView view(&scene);
    view.setRenderHint(QPainter::Antialiasing);
    view.setViewportUpdateMode(QGraphicsView::BoundingRectViewportUpdate);
    view.setBackgroundBrush(QColor(230, 200, 167));
    view.setWindowTitle("Drag and Drop Robot");
    view.show();

    return app.exec();
}

最後に、QGraphicsView ウィンドウを作成し、そのウィンドウにシーンを割り当てます。

視覚的な品質を高めるため、アンチエイリアシングを有効にします。また、視覚的な更新処理を簡略化するために、バウンディング・レクタングルの更新を使用するように設定します。ビューには、砂色の固定背景とウィンドウタイトルを設定します。

その後、ビューを表示します。制御がイベントループに入った直後にアニメーションが開始されます。

サンプルプロジェクト @ code.qt.io

© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.