拖拽机器人示例

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

拖拽机器人示例展示了如何在QGraphicsItem子类中实现拖拽,以及如何使用Qt的动画框架来动画化项。

图形视图提供了QGraphicsScene类来管理和与大量自定义的、继承自QGraphicsItem类的2D图形项交互,并提供了QGraphicsView小部件来可视化这些项,支持缩放和旋转。

该示例由一个Robot类、一个ColorItem类和主函数:Robot类定义了一个由几个RobotPart派生出的简单机器人肢体组成的机器人,包括RobotHeadRobotLimbColorItem类提供了一个可拖拽的颜色椭圆,而main()函数提供了主应用程序窗口。

我们将首先查看Robot类以了解如何组装不同的部分,以便它们可以使用QPropertyAnimation独立旋转和动画化,然后我们将查看ColorItem类来演示如何实现项之间的拖拽。最后我们将查看main()函数以了解我们如何将这些部分组合起来,以形成一个最终的应用程序。

机器人类定义

机器人由三个主要类组成:RobotHeadRobotTorsoRobotLimb,它是用于上臂和腿的。所有部分都继承自RobotPart类,而RobotPart类本身继承自QGraphicsObject。本身没有可视外观,仅作为机器人的根节点。

让我们从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。通过继承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() 来帮助为用户提供了 drag 已离开此项目的视觉反馈。

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

当拖放元素被放置到项目上(即在拖动时将鼠标按钮释放到项目上)时,会调用 dropEvent() 处理函数。

我们将 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(),以提供对图像拖放的特殊处理。

该类包含一个私有的 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()。我们检查拖拽对象是否包含图像数据,如果包含,我们将这些数据存储为一个成员位图,并调用update()。这个位图被用于我们之前审查过的paint()实现内部。

代码RobotTorsoRobotLimbRobotHead类似,所以我们直接跳到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()实现可以返回空的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 为其颜色成员分配一个不透明的随机颜色。为了提高可用性,它还分配了一个提示(tooltip),为用户提供有用的提示,并设置了一个合适的光标。这确保了当鼠标指针悬停在项目上时,光标将变为 Qt::OpenHandCursor

最后,我们调用 setAcceptedMouseButtons() 确保此项目只能处理 Qt::LeftButton。这大大简化了鼠标事件处理程序,因为我们总是可以假设只有左键被按下和释放。

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

项目的边界矩形是一个固定大小的 30x30 单位,围绕项目的原点 (0, 0) 中心定位,并在各个方向上调整了 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 渲染到一个新的位图中,为用户提供视觉反馈,表明颜色正在被“拖动”。

    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

© 2024 The Qt Company Ltd。本文件中包含的文档贡献的版权归其各自所有者所有。本文件提供的文档是根据由自由软件基金会发布的GNU自由文档许可版1.3的条款许可的。Qt及其相关标志是The Qt Company Ltd在芬兰及/或其他世界各地的商标。所有其他商标均为其各自所有者的财产。