本页内容

拖放机器人示例

演示如何在图形视图中拖放项目。

“拖放机器人”示例展示了如何在QGraphicsItem 的子类中实现拖放功能,以及如何使用Qt的动画框架对项目进行动画处理。

响应拖放事件的机器人应用

Graphics View 提供了QGraphicsScene 类,用于管理和与大量从QGraphicsItem 类派生的自定义 2D 图形项进行交互;同时还提供QGraphicsView 控件来可视化这些项,并支持缩放和旋转。

本示例由Robot 类、ColorItem 类和main函数组成:Robot 类描述了一个由多个从RobotPart 派生的肢体组成的简单机器人,包括RobotHead 和RobotLimb ;ColorItem 类提供了一个可拖动的彩色椭圆;main() 函数则提供主应用程序窗口。

我们将首先回顾Robot 类,了解如何组装各个部件,以便通过QPropertyAnimation 实现单独旋转和动画效果;随后将回顾ColorItem 类,演示如何在项目之间实现拖放功能。最后,我们将回顾main()函数,了解如何将所有组件整合在一起,形成最终的应用程序。

机器人类定义

机器人由三个主要类组成:RobotHead 、RobotTorso 以及RobotLimb ,其中 用于控制上臂、下臂和腿部。所有部件都继承自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 还实现了处理拖放事件的三个最重要的事件处理程序: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 ,因为该类存在一个细微差异,其余类则留作读者练习。

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() 中,我们绘制实际的头部。该实现分为两部分:如果已将图片拖放到头部上,则绘制该图片;否则,使用简单的矢量图形绘制一个圆角矩形的机器人头部。

出于性能考虑,根据绘制内容的复杂程度,将头部作为图像绘制通常比使用一系列矢量操作更快。

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);

下一节将创建所有动画对象。这段代码片段展示了控制头部缩放和旋转的两个动画。这两个QPropertyAnimation 实例仅需设置对象、属性以及相应的起始值和结束值即可。

所有动画均由一个顶级并行动画组控制。缩放和旋转动画已被添加到该组中。

其余动画的定义方式与此类似。

    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() 方法。该类包含一个私有颜色成员变量。

让我们来看看它的实现。

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) 为中心的固定 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 数据。同时,该位图也会被设为拖拽对象的位图。这样可以确保你能在鼠标光标下方看到正在被拖拽的图片作为位图显示。

    } 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() 函数

现在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 实例。将机器人缩放并向上移动几个单位,然后将其添加到场景中。

    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.