En esta página

Ejemplo de robot de arrastrar y soltar

Demuestra cómo arrastrar y soltar elementos en una vista gráfica.

El ejemplo de Arrastrar y Soltar Robot muestra cómo implementar Arrastrar y Soltar en una subclase QGraphicsItem, así como animar elementos utilizando el Marco de Animación de Qt.

Aplicación de un robot que responde a eventos de arrastrar y soltar

Graphics View proporciona la clase QGraphicsScene para gestionar e interactuar con un gran número de elementos gráficos 2D personalizados derivados de la clase QGraphicsItem, y un widget QGraphicsView para visualizar los elementos, con soporte para zoom y rotación.

Este ejemplo consta de una clase Robot, una clase ColorItem y una función principal: la clase Robot describe un robot simple formado por varias extremidades derivadas de RobotPart, incluyendo RobotHead y RobotLimb, la clase ColorItem proporciona una elipse de color arrastrable, y la función main() proporciona la ventana principal de la aplicación.

Primero revisaremos la clase Robot para ver cómo ensamblar las diferentes partes para que puedan ser rotadas y animadas individualmente usando QPropertyAnimation, y luego revisaremos la clase ColorItem para demostrar cómo implementar Arrastrar y Soltar entre elementos. Finalmente revisaremos la función main() para ver cómo podemos unir todas las piezas, para formar la aplicación final.

Definición de la Clase Robot

El robot consta de tres clases principales: el RobotHead, el RobotTorso, y el RobotLimb, que se utiliza para la parte superior e inferior de los brazos y las piernas. Todas las partes derivan de la clase RobotPart, que a su vez hereda de QGraphicsObject. La clase Robot en sí no tiene apariencia visual y sólo sirve como nodo raíz para el robot.

Comencemos con la declaración de la clase 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;
};

Esta clase base hereda de QGraphicsObject. QGraphicsObject proporciona señales y ranuras a través de la herencia de QObject, y también declara las propiedades de QGraphicsItem utilizando Q_PROPERTY, lo que hace que las propiedades sean accesibles para QPropertyAnimation.

RobotPart también implementa los tres manejadores de eventos más importantes para aceptar eventos de caída: dragEnterEvent(), dragLeaveEvent(), y dropEvent().

El color se almacena como una variable miembro, junto con la variable dragOver, que utilizaremos más adelante para indicar visualmente que la extremidad puede aceptar colores que se arrastran sobre ella.

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

RobotPartEl constructor de esta clase inicializa el miembro dragOver y establece el color a Qt::lightGray. En el cuerpo del constructor habilitamos el soporte para aceptar eventos de caída llamando a setAcceptDrops(true).

El resto de la implementación de esta clase es para soportar Drag and Drop.

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

El manejador dragEnterEvent() es llamado cuando un elemento Drag and Drop es arrastrado dentro del área de la parte robótica.

La implementación del manejador determina si este elemento en su conjunto puede aceptar o no los datos mime asociados con el objeto de arrastre entrante. RobotPart proporciona un comportamiento base para todas las piezas que acepta las caídas de color. Así que si el objeto de arrastre entrante contiene un color, el evento es aceptado, establecemos dragOver a true y llamamos a update() para ayudar a proporcionar una retroalimentación visual positiva al usuario; de lo contrario, el evento es ignorado, lo que a su vez permite que el evento se propague a los elementos padre.

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

El manejador dragLeaveEvent() es llamado cuando un elemento Drag and Drop es arrastrado fuera del área de la parte robótica. Nuestra implementación simplemente restablece dragOver a false y llama a update() para ayudar a proporcionar retroalimentación visual de que el arrastre ha dejado este elemento.

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

El controlador dropEvent() se ejecuta cuando un elemento de arrastrar y soltar se suelta sobre un elemento (es decir, cuando se suelta el botón del ratón sobre el elemento mientras se arrastra).

Restablecemos dragOver a false, asignamos el nuevo color al elemento y llamamos a update().

La declaración e implementación de RobotHead, RobotTorso, y RobotLimb son prácticamente idénticas. Revisaremos RobotHead en detalle, ya que esta clase tiene una pequeña diferencia, y dejaremos las otras clases como ejercicio para el lector.

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

La clase RobotHead hereda de RobotPart y proporciona las implementaciones necesarias de boundingRect() y paint(). También reimplementa dragEnterEvent() y dropEvent() para proporcionar un manejo especial de las caídas de imágenes.

La clase contiene un miembro privado pixmap que podemos utilizar para implementar el soporte para aceptar caídas de imágenes.

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

RobotHead tiene un constructor bastante simple que simplemente reenvía al constructor de RobotPart.

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

La reimplementación de boundingRect() devuelve las extensiones de la cabeza. Como queremos que el centro de rotación sea el centro inferior del elemento, hemos elegido un rectángulo delimitador que comienza en (-15, -50) y se extiende hasta 30 unidades de ancho y 50 unidades de alto. Al rotar la cabeza, el "cuello" permanecerá inmóvil mientras que la parte superior de la cabeza se inclina de lado a lado.

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

En paint() dibujamos la cabeza real. La implementación se divide en dos secciones: si se ha dejado caer una imagen sobre la cabeza, dibujamos la imagen; de lo contrario, dibujamos una cabeza de robot rectangular redonda con gráficos vectoriales simples.

Por razones de rendimiento, dependiendo de la complejidad de lo que se pinta, a menudo puede ser más rápido dibujar la cabeza como una imagen en lugar de utilizar una secuencia de operaciones vectoriales.

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

La cabeza del robot puede aceptar caídas de imagen. Con el fin de apoyar esto, su reimplementación de dragEnterEvent() comprueba si el objeto de arrastre contiene datos de imagen, y si lo hace, entonces el evento es aceptado. En caso contrario, volvemos a la implementación base de RobotPart.

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

Para seguir con el soporte de imágenes, también debemos implementar dropEvent(). Comprobamos si el objeto de arrastre contiene datos de imagen, y si es así, almacenamos estos datos como un pixmap miembro y llamamos a update(). Este pixmap se utiliza dentro de la implementación de paint() que revisamos antes.

RobotTorso y RobotLimb son similares a RobotHead, así que pasemos directamente a la clase 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;
};

La clase Robot también hereda de RobotPart, y al igual que las otras partes también implementa boundingRect() y paint(). Sin embargo, proporciona una implementación bastante especial:

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

Dado que la clase Robot sólo se utiliza como nodo base para el resto del robot, no tiene representación visual. Por lo tanto, su implementación boundingRect() puede devolver un QRectF nulo, y su función paint() no hace nada.

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

El constructor comienza estableciendo la bandera ItemHasNoContents, que es una optimización menor para los elementos que no tienen apariencia visual.

A continuación, construimos todas las partes del robot (cabeza, torso y brazos y piernas). El orden de apilamiento es muy importante, y utilizamos la jerarquía padre-hijo para asegurarnos de que los elementos giran y se mueven correctamente. Primero se construye el torso, que es el elemento raíz. A continuación, construimos la cabeza y pasamos el torso al constructor de HeadItem. Esto hará que la cabeza sea hija del torso; si giras el torso, la cabeza lo seguirá. El mismo patrón se aplica al resto de las extremidades.

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

Cada parte del robot se coloca cuidadosamente. Por ejemplo, el brazo superior izquierdo se mueve con precisión a la zona superior izquierda del torso, y el brazo superior derecho se mueve a la zona superior derecha.

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

La siguiente sección crea todos los objetos de animación. Este fragmento muestra las dos animaciones que operan sobre la escala y la rotación de la cabeza. Las dos instancias de QPropertyAnimation simplemente establecen el objeto, la propiedad y los respectivos valores de inicio y fin.

Todas las animaciones son controladas por un grupo de animación paralelo de nivel superior. Las animaciones de escala y rotación se añaden a este grupo.

El resto de las animaciones se definen de forma similar.

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

Finalmente establecemos una curva de relajación y duración en cada animación, aseguramos que el grupo de animación de nivel superior se repita para siempre, e iniciamos la animación de nivel superior.

Definición de la clase ColorItem

La clase ColorItem representa un ítem circular que puede ser presionado para arrastrar colores sobre las partes del robot.

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

Esta clase es muy simple. No utiliza animaciones, y no tiene necesidad de propiedades ni señales y ranuras, por lo que para ahorrar recursos, lo más natural es que herede QGraphicsItem (en lugar de QGraphicsObject).

Declara las funciones obligatorias boundingRect() y paint(), y añade reimplementaciones de mousePressEvent(), mouseMoveEvent(), y mouseReleaseEvent(). Contiene un único miembro privado color.

Veamos su implementación.

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

ColorItemEl constructor de 's asigna un color opaco aleatorio a su miembro color haciendo uso de QRandomGenerator. Para mejorar la usabilidad, asigna un tooltip que proporciona una pista útil al usuario, y también establece un cursor adecuado. Esto garantiza que el cursor se muestre en Qt::OpenHandCursor cuando el puntero del ratón pase por encima del elemento.

Por último, llamamos a setAcceptedMouseButtons() para asegurarnos de que este elemento sólo puede procesar Qt::LeftButton. Esto simplifica enormemente los manejadores de eventos del ratón, ya que siempre podemos asumir que sólo se pulsa y suelta el botón izquierdo del ratón.

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

El rectángulo delimitador del ítem es de 30x30 unidades centradas alrededor del origen del ítem (0, 0), y ajustado en 0.5 unidades en todas las direcciones para permitir que un lápiz escalable dibuje su contorno. Para un toque visual final, los límites también se compensan con unas pocas unidades hacia abajo y hacia la derecha para dejar espacio para una simple sombra.

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

La implementación de paint() dibuja una elipse con un contorno negro de 1 unidad, un relleno de color liso y una sombra gris oscura.

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

El manejador mousePressEvent() es llamado cuando se pulsa el botón del ratón dentro del área del elemento. Nuestra implementación simplemente coloca el cursor en Qt::ClosedHandCursor.

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

El manejador mouseReleaseEvent() es llamado cuando sueltas el botón del ratón después de haberlo presionado dentro del área de un elemento. Nuestra implementación devuelve el cursor a Qt::OpenHandCursor. Los manejadores de eventos de pulsar y soltar el ratón juntos proporcionan una útil retroalimentación visual al usuario: cuando mueves el puntero del ratón sobre un CircleItem, el cursor cambia a una mano abierta. Al pulsar el elemento, se mostrará un cursor de mano cerrada. Al soltarlo, el cursor vuelve a ser una mano abierta.

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

El manejador mouseMoveEvent() es llamado cuando mueves el ratón después de presionar el botón del ratón dentro del área de ColorItem. Esta implementación proporciona la pieza más importante de la lógica para CircleItem: el código que inicia y gestiona los arrastres.

La implementación comienza comprobando si el ratón ha sido arrastrado lo suficiente como para eliminar el ruido del jitter del ratón. Sólo queremos iniciar un arrastre si el ratón ha sido arrastrado más lejos que la distancia de arrastre inicial de la aplicación.

Continuando, creamos un objeto QDrag, pasando el evento widget (es decir, el viewport QGraphicsView ) a su constructor. Qt se asegurará de que este objeto se elimine en el momento adecuado. También creamos una instancia QMimeData que puede contener nuestros datos de color o imagen, y la asignamos al objeto de arrastre.

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

Este snippet tiene un resultado algo aleatorio: de vez en cuando, una imagen especial es asignada a los datos mime del objeto drag. El pixmap también se asigna como pixmap del objeto de arrastre. Esto asegurará que se pueda ver la imagen que se está arrastrando como un pixmap bajo el cursor del ratón.

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

De lo contrario, y este es el resultado más común, se asigna un simple color a los datos mime del objeto de arrastre. Renderizamos este ColorItem en un nuevo pixmap para dar al usuario información visual de que el color está siendo "arrastrado".

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

Finalmente ejecutamos el arrastre. QDrag::exec() volverá a entrar en el bucle de eventos, y sólo saldrá si el arrastre ha sido soltado o cancelado. En cualquier caso reiniciamos el cursor a Qt::OpenHandCursor.

La función main()

Ahora que las clases Robot y ColorItem están completas, podemos juntar todas las piezas dentro de la función main().

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

Comenzamos construyendo QApplication, e inicializando el generador de números aleatorios. Esto asegura que los elementos de color tengan colores diferentes cada vez que se inicie la aplicación.

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

Construimos una escena de tamaño fijo y creamos 10 instancias de ColorItem dispuestas en círculo. Cada elemento se añade a la escena.

En el centro de este círculo creamos una instancia de Robot. El robot se escala y se desplaza unas unidades hacia arriba. A continuación, se añade a la escena.

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

Por último, creamos una ventana QGraphicsView y le asignamos la escena.

Para aumentar la calidad visual, activamos el antialiasing. También optamos por utilizar actualizaciones de rectángulos delimitadores para simplificar el manejo de las actualizaciones visuales. A la vista se le asigna un fondo fijo de color arena y un título de ventana.

A continuación, mostramos la vista. Las animaciones comienzan inmediatamente después de que el control entra en el bucle de eventos.

Proyecto de ejemplo @ 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.