Drag and drop from QListWidget onto QGraphicsItem

This article demonstrates how to implement drag and drop of a text from QListWidget onto a QGraphicsItem. Two variants of the problem are presented:

  1. Variant 1 With reuse - the text won't be deleted from the QListWidget after it has been dropped onto a QGraphicsItem

  2. Variant 2 No reuse - the text will be deleted from the QListWidget after it has been dropped onto a QGraphicsItem. The text cannot be dragged back to the QListWidget, however, the text will reappear in the QListWidget once the user selects an item/items and presses the delete key.

To achieve the desired functionality we need to do the following:

The variants differ only in the implementation of dropEvent(QGraphicsSceneDragDropEvent *event) in the derived CustomRectItem and in keyPressEvent(QKeyEvent *event) in the derived MainWindow.

How to subclass QGraphicsRectItem

Below is the header of the subclassed QGraphicsRectItem which should be able to accept drops. Since we also want the item to accept and display text, we hold the text as a private variable, add an accessor and mutator method and override the paint method to display the text. To accomplish the actual drag and drop we have to implement the dropEvent().

class CustomRectItem : public QGraphicsRectItem
{
public:
    CustomRectItem(QGraphicsItem * parent = 0);
    void dropEvent(QGraphicsSceneDragDropEvent *event);
    void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget);
    void setText(const QString& text);
    QString text() const {return m_text;}
private:
    QString m_text;
};

In the customrectitem.cpp notice that in the CustomRectItem's constructor we enable drops and make the item selectable, so that the text can be removed from the items if desired.

CustomRectItem::CustomRectItem(QGraphicsItem *parent):
    QGraphicsRectItem(parent)
{
    setAcceptDrops(true);
    setFlags(QGraphicsItem::ItemIsSelectable);
}

void CustomRectItem::setText(const QString &text){
    this->m_text = text;
    update();
}

void CustomRectItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option,
           QWidget *widget){
    QGraphicsRectItem::paint(painter, option, widget);
    painter->drawText(boundingRect(),m_text,
                      QTextOption(Qt::AlignCenter));
}

Implementation of dropEvent() for Variant 1

Variant 1 refers to the scenario in which the items aren't deleted from the list widget after they are dropped on the QGraphicsItem. The implementation of the dropEvent() for this variant can be seen below. The idea is to retrieve the dragged data with the help of the event's mime data (see the note below).

void CustomRectItem::dropEvent(QGraphicsSceneDragDropEvent *event){
     if (event->mimeData()->hasFormat("application/x-qabstractitemmodeldatalist")){
         QByteArray itemData = event->mimeData()->data(
                     "application/x-qabstractitemmodeldatalist");
         QDataStream stream(&itemData, QIODevice::ReadOnly);
         int row, col;
         QMap<int, QVariant> valueMap;
         stream >> row >> col >> valueMap;
         if(!valueMap.isEmpty())
            setText(valueMap.value(0).toString());
         event->accept();
     }
     else
         event->ignore();
}

We accept the event if it has the desired format, i.e. application/x-qabstractitemmodeldatalist and ignore it if it doesn't. We then decode the data by storing it in QByteArray and deconstructing it into row, column and QMap<int, QVariant> with the help of QDataStream. In our case, the map has only one element and stores the text that is being dragged. We retrieve the value from the map, convert it to QString and set the decoded value as the text of our item.

Note: QMimeData is used to describe information that can be stored in the clipboard and transferred via the drag and drop mechanism. The mimetype format of the data that we are automatically getting from QListWidget is application/x-qabstractitemmodeldatalist (this is because there is a QAbstractItemModel underlying the QListWidget).

The mime data for application/x-qabstractitemmodeldatalist format contains the row, column and QMap<int, QVariant> for an index that is being dragged. E.g. dragging the orange color would result in the following decoded mime data:

qDebug() << row << "," << col << "," << valueMap;

2, 0, QMap((0, QVariant(QString, "orange")))

The debugging output represents the row 2 of the list widget (i.e. the third row since the numbering starts at 0) and column 0 (the list widget has a single column). In our case, the only information we are interested in are the contents of the map. In our case, the map contains a single element, which is the color being dragged.

Implementation of dropEvent() for Variant 2

Variant 2 refers to the scenario in which the text items are deleted from the list widget after they are dropped onto the QGraphicsItem. This is ensured by a single command event->setDropAction(Qt::MoveAction) (in Variant 1 we used the default action, i.e. Qt::CopyAction). We also handle the case, in which the QGraphicsItem already contains some text. In such case, this text needs to be reinstated as an item of the QListWidget instance. This is accomplished with the method QListWidget::addItem() applied to the source of the event.

void CustomRectItem::dropEvent(QGraphicsSceneDragDropEvent *event){
     if (event->mimeData()->hasFormat("application/x-qabstractitemmodeldatalist")){
         QByteArray itemData = event->mimeData()->data(
                     "application/x-qabstractitemmodeldatalist");
         QDataStream stream(&itemData, QIODevice::ReadOnly);
         int row, col;
         QMap<int, QVariant> valueMap;
         stream >> row >> col >> valueMap;
         QString origText = m_text;
         if(!valueMap.isEmpty())
            setText(valueMap.value(0).toString());

         event->setDropAction(Qt::MoveAction);
         event->accept();

         if(!origText.isEmpty()){
             if(QListWidget* lWidget = qobject_cast<QListWidget*> (event->source()))
                  lWidget->addItem(origText);
         }
     }
     else
         event->ignore();
}

Now that we subclassed the QGraphicsRectItem and implemented its dropEvent() we can see the item in action.

MainWindow implementation

Below you can see header mainwindow.h. The MainWindow holds a pointer to a QListWidget, QGraphicsView and QGraphicsScene instance and private methods to instantiate them and populate the scene with items. The method keyPressEvent is also reimplemented. This is so that we can remove the text from selected QGraphicsItems with the delete key.

class MainWindow : public QMainWindow
{
public:
    MainWindow();
protected:
    void keyPressEvent(QKeyEvent *);
private:
    QListWidget* itemListWidget;
    QGraphicsView* view;
    QGraphicsScene* scene;

    void createListWidget();
    void createSceneAndView();
    void createGraphicsItems();
    void createLayout();
};

Below you can see the constructor and the private methods in which we instantiate the list widget, scene and the view. The code related to the population of the scene with QGraphicsItems can be found in the files above. Notice that to make our implementation work, we have to call QListWidget::setDragEnabled(true)

MainWindow::MainWindow()
{
    setWindowTitle("Drag and drop from QListWidget v1");
    resize(350,250);

    createListWidget();
    createSceneAndView();
    createGraphicsItems();
    createLayout();
}

void MainWindow::createListWidget(){
    itemListWidget = new QListWidget;
    QStringList itemList;
    itemList << "gray" << "blue" << "orange";
    itemListWidget->addItems(itemList);
    itemListWidget->setFixedWidth(100);
    itemListWidget->setDragEnabled(true);
}

void MainWindow::createSceneAndView(){
    scene = new QGraphicsScene(this);
    scene->setSceneRect(0,0,200,200);
    view = new QGraphicsView;
    view->setScene(scene);
}

In createLayout() we create a horizontal layout and a frame object which will serve as a central widget. The QHBoxLayout contains a QListWidget and a QGraphicsView object. This is the same for both variants.

void MainWindow::createLayout(){
    QFrame *frame = new QFrame;
    QHBoxLayout *frameLayout = new QHBoxLayout(frame);
    frameLayout->addWidget(itemListWidget);
    frameLayout->addWidget(view);
    setCentralWidget(frame);
}

Last, we override the keyPressEvent(). Anytime the user presses the delete key, the text will be removed from the selected QGraphicsItems (i.e. the text will be set to a null string).

MainWindow::keyPressEvent() for Variant 1

void MainWindow::keyPressEvent(QKeyEvent *event){
    if(event->key() == Qt::Key_Delete){
        foreach(QGraphicsItem *item, scene->selectedItems())
            if(CustomRectItem *rItem = qgraphicsitem_cast<CustomRectItem*> (item))
                rItem->setText(QString());
        scene->clearSelection();
    }
    QMainWindow::keyPressEvent(event);
}

MainWindow::keyPressEvent() for Variant 2

For Variant 2 of the problem we also need to return the text deleted from the QGraphicsItems to the QListWidget. This is accomplished with QListWidget::addItem(QString).

void MainWindow::keyPressEvent(QKeyEvent *event){
    if(event->key() == Qt::Key_Delete){
        foreach(QGraphicsItem *item, scene->selectedItems())
            if(CustomRectItem *rItem = qgraphicsitem_cast<CustomRectItem*> (item)){
                itemListWidget->addItem(rItem->text());
                rItem->setText(QString());
            }
        scene->clearSelection();
    }
    QMainWindow::keyPressEvent(event);
}

That's it! Now you can try to drag text items from the list widget and drop them on to QGraphicsItems.


Created: 05.07.2016
© Walletfox.com, 2017