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

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

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

グラフィックス・ビューは、QGraphicsItem クラスから派生した多数のカスタムメイドの 2D グラフィカル・アイテムを管理し、操作するためのQGraphicsScene クラスと、ズームと回転をサポートしたアイテムを視覚化するためのQGraphicsView ウィジェットを提供します。

この例は、Robot クラス、ColorItem クラス、およびメイン関数で構成されています。Robot クラスは、RobotHeadRobotLimb など、RobotPart から派生したいくつかの手足で構成される単純なロボットを記述し、ColorItem クラスはドラッグ可能な色付き楕円を提供し、main() 関数はメイン・アプリケーション・ウィンドウを提供します。

まず、Robot クラスをレビューして、QPropertyAnimation を使って、さまざまなパーツを個別に回転させたり、アニメーションさせたりできるようにする方法を確認します。次に、ColorItem クラスをレビューして、アイテム間のドラッグ&ドロップを実装する方法を示します。最後に、main()関数を復習して、どのようにすべてのパーツを組み合わせて最終的なアプリケーションを構成するかを確認します。

ロボット・クラスの定義

このロボットは、RobotHeadRobotTorsoRobotLimb の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;
};

QPropertyAnimationこの基本クラスはQGraphicsObject を継承します。QGraphicsObjectQObject を継承してシグナルとスロットを提供し、Q_PROPERTY を使用してQGraphicsItem のプロパティを宣言します。

また、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() ハンドラは、Drag and Drop 要素がロボット部品の領域にドラッグされたときに呼び出されます。

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

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

dragLeaveEvent() ハンドラは、Drag and Drop 要素がロボット部品の領域からドラッグされたときに呼び出されます。この実装では、単にdragOverを false にリセットし、update() を呼び出して、ドラッグがこのアイテムから離れたことを視覚的にフィードバックします。

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

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

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

RobotHeadRobotTorsoRobotLimb の宣言と実装はほとんど同じです。RobotHead 、このクラスにはちょっとした違いがあるので詳しく説明します。

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() を再実装し、画像ドロップの特別な処理を提供しています。

このクラスにはprivate 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() では、実際の頭部を描画します。頭部に画像がドロップされている場合はその画像を描画し、そうでない場合は単純なベクターグラフィックスで丸い長方形のロボット頭部を描画します。

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

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() の実装は nullQRectF を返すことができ、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つのアニメーションを示しています。つの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 ()の再実装を追加している。この関数には、private colorメンバが1つ含まれている。

その実装を見てみよう。

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コンストラクタのcolorメンバには、QRandomGenerator を利用して、不透明なランダム色が代入されます。ユーザビリティを向上させるために、ユーザに有用なヒントを提供するツールチップが代入され、適切なカーソルも設定されます。これにより、マウス・ポインタがアイテムの上にあるとき、カーソルがQt::OpenHandCursor になることを保証する。

最後に、setAcceptedMouseButtons() を呼び出して、このアイテムがQt::LeftButton のみを処理できるようにします。これにより、マウスの左ボタンだけが押されたり離されたりしていると常に仮定できるので、マウス・イベント・ハンドラが非常に単純化されます。

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

アイテムの外接矩形は、アイテムの原点 (0, 0) を中心とした 30x30 の固定矩形で、スケーラブルなペンで輪郭を描けるように、すべての方向に 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);

ColorItem の領域内でマウスボタンを押した後、マウスを動かすとmouseMoveEvent() ハンドラが呼び出されます。この実装は、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データに割り当てられます。このpixmapはドラッグオブジェクトのpixmapとしても割り当てられます。これにより、マウスカーソルの下にドラッグされる画像がpixmapとして見えるようになります。

    } 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 を新しいpixmapにレンダリングして、その色が「ドラッグ」されているという視覚的なフィードバックをユーザーに与えます。

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

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

main()関数

これでRobotColorItem のクラスが完成したので、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

©2024 The Qt Company Ltd. 本書に含まれる文書の著作権は、それぞれの所有者に帰属します。 本書で提供されるドキュメントは、Free Software Foundation が発行したGNU Free Documentation License version 1.3に基づいてライセンスされています。 Qtおよびそれぞれのロゴは、フィンランドおよびその他の国におけるThe Qt Company Ltd.の 商標です。その他すべての商標は、それぞれの所有者に帰属します。