PDA

View Full Version : QListView drag and drop



buster
9th August 2020, 16:33
I have a dialogue with two QListView objects A and B, and I want to drag text strings from A to B, and vice verse. In my sub-classed QListview, ListView, I have populated A's model from a QStringList, and enabled the obejcts thus:



m_model_A = new QStringListModel;
m_listview_A = new ListView(this);
m_listview_A->setModel(m_model_A);
m_listview_A->setSelectionMode(QAbstractItemView::ExtendedSelect ion);
m_listview_A->setDragEnabled(true);
m_listview_A->setAcceptDrops(true);
m_listview_A->setDropIndicatorShown(true);

Idem for m_model_B / m_listview_B.

I have found several examples of drag and drop using QListWidget, but not for QListView. So I am unsure about what is already implemented in QListView. Subclassing QListView and reimplementing only dropEvent this function is called, but not if I also reimplement dragEnterEvent. Which cased my to wonder if dragEnterEvent perhaps does not need to be reimplemented for QListView? Also,


if ( event->mimeData()->hasText())

in dropEvent(QDropEvent *event) evaluates to false. So, do I need to reimplement dragEnterEvent and perhaps mousePressEvent when using QListView?

d_stranz
9th August 2020, 23:44
This simple program supports drag and drop for list A (left) to list B (right) and vice versa. No extra code.



// main.cpp
#include "ListViewDragDrop.h"
#include <QtWidgets/QApplication>

int main(int argc, char *argv[])
{
QApplication a(argc, argv);
ListViewDragDrop w;
w.show();
return a.exec();
}




// ListViewDragDrop.h

#ifndef LISTVIEWDRAGDROP_H
#define LISTVIEWDRAGDROP_H

#include <QtWidgets/QMainWindow>

class QListView;
class QStringListModel;

class ListViewDragDrop : public QMainWindow
{
Q_OBJECT

public:
ListViewDragDrop(QWidget *parent = 0);
~ListViewDragDrop();

private:
QStringListModel * mpModelA;
QStringListModel * mpModelB;

QListView * mpViewA;
QListView * mpViewB;
};

#endif // LISTVIEWDRAGDROP_H




// ListViewDragDrop.cpp

#include "ListViewDragDrop.h"

#include <QListView>
#include <QStringListModel>
#include <QSplitter>

ListViewDragDrop::ListViewDragDrop(QWidget *parent)
: QMainWindow(parent)
{
QStringList listA;
listA << "String A1" << "String A2" << "String A3" << "String A4";

mpModelA = new QStringListModel( this );
mpModelA->setStringList( listA );

QStringList listB;
listB << "String B1" << "String B2" << "String B3" << "String B4";

mpModelB = new QStringListModel( this );
mpModelB->setStringList( listB );

QSplitter * pSplitter = new QSplitter( Qt::Horizontal );

mpViewA = new QListView();
mpViewA->setModel( mpModelA );
mpViewA->setDragEnabled( true );
mpViewA->setAcceptDrops( true );

mpViewB = new QListView();
mpViewB->setModel( mpModelB );
mpViewB->setDragEnabled( true );
mpViewB->setAcceptDrops( true );

pSplitter->addWidget( mpViewA );
pSplitter->addWidget( mpViewB );
setCentralWidget( pSplitter );

}

ListViewDragDrop::~ListViewDragDrop()
{
}


Note that the default CLICK-DRAG operation is to copy items from one list to the other, so you get duplicates. If you want to move from one list to the other, then you need to derive from QStringListModel and override the QStringListModel::supportedDropActions() method to return Qt::MoveAction



#ifndef MOVELISTMODEL_H
#define MOVELISTMODEL_H

#include <QStringListModel>

class MoveListModel : public QStringListModel
{
Q_OBJECT

public:
MoveListModel( QObject *parent ) : QStringListModel( parent ) {}
~MoveListModel() {}

virtual Qt::DropActions supportedDropActions() const override
{
return Qt::MoveAction;
}

private:

};
#endif // MOVELISTMODEL_H


With this code, if you make mpModelA a MoveListModel (and leave mpModelB alone), then if you select an item from list A and drag it to list B, it will be moved. However, because the default drop action Qt::CopyOption is not in these supportedDropActions flags, you cannot simply copy from B to A; on Windows, you must SHIFT-CLICK-DRAG from list B to move the item from list B to A. There is no longer any way to simply copy from B to A.

If you change the flags to return both copy and move:


virtual Qt::DropActions supportedDropActions() const override
{
return Qt::CopyAction | Qt::MoveAction;
}


Now, the default CLICK-DRAG from list A to list B will move the item. A CTRL-CLICK-DRAG will copy the item. However, for list B (which is using the standard QStringListModel), the default CLICK-DRAG will copy the item from B to A, CTRL-CLICK-DRAG also copies the item, and SHIFT-CLICK-DRAG moves it.

Clear as mud, right? :-)

buster
14th August 2020, 14:23
This works, in the sense that the text strings can be moved and appear in the target view. But I have not been able to read out the data dropped on the target view. If I read it out like this


QStringList listeB = mpModelB->stringList();

I get the original data in listeB, but not the data I dropped onto it. But since it is displayed by viewB it must have been added to the model? What am I missing here?

d_stranz
14th August 2020, 17:05
But since it is displayed by viewB it must have been added to the model? What am I missing here?

It seems logical that it should be added to the model. According to the docs, the stringList() method should reflect what is currently in the model.

I would suggest looking at the model's rowCount() value. If it changes with drags and drops, it should indicate that the model is being changed. You could also connect to the dataChanged() signal.

buster
14th August 2020, 21:56
It seems logical that it should be added to the model. According to the docs, the stringList() method should reflect what is currently in the model.

I would suggest looking at the model's rowCount() value. If it changes with drags and drops, it should indicate that the model is being changed. You could also connect to the dataChanged() signal.

I finally got it working. Or almost. I subclassed QListView and reimplemented dropEvent. I did try that before as well, but it turned out I needed


QListView::dropEvent(e);

Apparently, I was breaking the event loop. I than called rowCount() and stringList(), as you suggested, and now it works. I should perhaps mention that I also subclassed QStringListModel, as you suggested in a previous post, in order to get a move, rather than a copy action, which also works fine.

There is a small hick-up, however, as the row count, and the data, of the source listview are not updated until I do a local move in the destination listview. As a test I put a pushbutton on the widget and read the row count and data manually and then I get the correct values. So it seems calling the read function from the dropEvent function is a bit too early. So I wonder if there is a way to detect that the move has been compleated?

d_stranz
15th August 2020, 15:39
So it seems calling the read function from the dropEvent function is a bit too early.

When are you calling QListView::dropEvent()? At the start of your own event handler or at the end? It is possible that there could be other events that get queued up that can't be acted on until your own event handler exits. Try calling processEvents() after calling the QListView::dropEvent() to see if that helps.

I am not really happy with that, though. It doesn't seem right that you should have to derive from QListView just to get the model updated. All of that should be built in - if it supports drops in the first place, it should be taking care of updating the model.

Can you post some code from your original implementation where asking for the string list returns the wrong result?

buster
15th August 2020, 18:23
Can you post some code from your original implementation where asking for the string list returns the wrong result?


My latest version looks like this (I use KDevelop/cmake):


#ifndef LISTVIEWDRAGDROP_H
#define LISTVIEWDRAGDROP_H

#include <QMainWindow>
#include <QPushButton>
#include "modmodel.h"

namespace Ui {
class ListViewDragDrop;
}

class QListView;
class ModModel;
class ModListView;

class ListViewDragDrop : public QMainWindow
{
Q_OBJECT

public:
explicit ListViewDragDrop(QWidget *parent = 0);
~ListViewDragDrop();

void receive_strings();

private:
//void slot_test();
Ui::ListViewDragDrop *ui;
QPushButton *m_button;

public:
//QStringListModel *m_ModelA;
//QStringListModel *m_ModelB;
ModModel *m_ModelA;
ModModel *m_ModelB;

ModListView *m_ViewA;
ModListView *m_ViewB;
};

#include "listviewdragdrop.h"
#include "ui_listviewdragdrop.h"
#include <QListView>
#include <QStringListModel>
#include <QSplitter>
#include <QDebug>
#include "modmodel.h"
#include "modlistview.h"

ListViewDragDrop::ListViewDragDrop(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::ListViewDragDrop)
{
ui->setupUi(this);

QStringList listA;
listA << "A1" << "A2" << "A3" << "A4";

//m_ModelA = new QStringListModel(this);
m_ModelA = new ModModel(this);
m_ModelA->setStringList(listA);

QStringList listB;
listB << "B1" << "B2" << "B3" << "B4";

//m_ModelB = new QStringListModel(this);
m_ModelB = new ModModel(this);
m_ModelB->setStringList(listB);

QSplitter * pSplitter = new QSplitter( Qt::Horizontal );

m_ViewA = new ModListView(this);
m_ViewA->setModel( m_ModelA );
m_ViewA->setDragEnabled( true );
m_ViewA->setAcceptDrops( true );

m_ViewB = new ModListView(this);
m_ViewB->setModel( m_ModelB );
m_ViewB->setDragEnabled( true );
m_ViewB->setAcceptDrops( true );

m_button = new QPushButton("Test");

pSplitter->addWidget(m_ViewA);
pSplitter->addWidget(m_ViewB);
pSplitter->addWidget(m_button);
setCentralWidget( pSplitter );
}

ListViewDragDrop::~ListViewDragDrop()
{
delete ui;
}

void ListViewDragDrop::receive_strings()
{
int ARC = m_ModelA->rowCount();
int BRC = m_ModelB->rowCount();
QStringList listeA = m_ModelA->stringList();
QStringList listeB = m_ModelB->stringList();
qDebug() << "ListViewDragDrop::receive_strings: rowo count:" << ARC << "listeB" << listeA << BRC << "listeB" << listeB;
}

#ifndef MODLISTVIEW_H
#define MODLISTVIEW_H

#include <QListView>
#include <QDropEvent>

class ListViewDragDrop;

class ModListView : public QListView
{
Q_OBJECT

public:
ModListView(ListViewDragDrop *parent);
~ModListView();

private:
void dropEvent(QDropEvent *e);
ListViewDragDrop *m_parent;
QPoint startPos;
};

#endif // MODLISTVIEW_H


#include "modlistview.h"
#include <QDebug>
#include <QDropEvent>
#include <QApplication>
#include <QMimeData>
#include <QAbstractItemView>
#include "listviewdragdrop.h"

ModListView::ModListView(ListViewDragDrop *parent) : m_parent(parent)
{
}

ModListView::~ModListView()
{
}

void ModListView::dropEvent(QDropEvent *e)
{
qDebug() << "ModListView::dropEvent: rowo count:";
QListView::dropEvent(e);
m_parent->receive_strings();
}

#ifndef MODMODEL_H
#define MODMODEL_H

#include <QStringListModel>
#include <QDropEvent>

class ListViewDragDrop;

class ModModel : public QStringListModel
{
Q_OBJECT

public:
ModModel(ListViewDragDrop *parent);
~ModModel();

virtual Qt::DropActions supportedDropActions() const override
{
return Qt::MoveAction;
}

private:
ListViewDragDrop *m_parent;

};

#endif // MODMODEL_H

#include "modmodel.h"
#include <QDebug>
#include "listviewdragdrop.h"

ModModel::ModModel(ListViewDragDrop *parent) : m_parent(parent)
{
}

ModModel::~ModModel()
{
}

d_stranz
15th August 2020, 21:11
void ModListView::dropEvent(QDropEvent *e)
{
qDebug() << "ModListView::dropEvent: rowo count:";
QListView::dropEvent(e);
m_parent->receive_strings();
}

You aren't letting control return to the event loop before trying to retrieve the listview contents. In particular, the things that the model and view needed to do to communicate with each other what has been dropped, and for the model to signal its view that the model has changed.

In your MainWindow class, make a slot to handle the QStringListModel' s dataChanged() signal (see QAbstractItemModel). Connect the model's signal to your slot. In that slot, you should be able to retrieve the updated string list.

I think you can probably get rid of your ModListView class completely. The standard QListView does what you need. You just need to use it correctly.

buster
16th August 2020, 00:48
I tried hooking up to dataChanged, as per your suggestion, but the behaviour is exactly the same as before. The listview I move from (listA) is visually updated (so the model must also be correctly updated), but what I read out of the from the model (in a slot connected to dataChanged) is not correct. If I drag an item in the destination view (viewB) to another row, then the source view (viewA) data is updated, but now the destination data read out in modelB is not; the source item is not deleted (visually it is), so I get one row too many. If I now move a row in listA, then listB is updated, but now list A is behind. So I am still scratching my head...

d_stranz
16th August 2020, 16:46
I modified the program I posted above to add slots for various model methods (dataChanged, rowsInserted, rowsRemoved):



// ListViewDragDrop.h
#ifndef LISTVIEWDRAGDROP_H
#define LISTVIEWDRAGDROP_H

#include <QtWidgets/QMainWindow>

class QListView;
class QStringListModel;
class QModelIndex;

class ListViewDragDrop : public QMainWindow
{
Q_OBJECT

public:
ListViewDragDrop(QWidget *parent = 0);
~ListViewDragDrop();

protected slots:
void onModelADataChanged( const QModelIndex & topLeft, const QModelIndex & bottomRight );
void onModelARowsInserted( const QModelIndex & parent, int first, int last );
void onModelARowsRemoved( const QModelIndex & parent, int first, int last );
void onModelBDataChanged( const QModelIndex & topLeft, const QModelIndex & bottomRight );
void onModelBRowsInserted( const QModelIndex & parent, int first, int last );
void onModelBRowsRemoved( const QModelIndex & parent, int first, int last );

private:
void dumpStringList( const QString & where, const QString & modelName, QStringListModel * pModel );

private:
QStringListModel * mpModelA;
QStringListModel * mpModelB;

QListView * mpViewA;
QListView * mpViewB;
};

#endif // LISTVIEWDRAGDROP_H




// ListViewDragDrop.cpp
#include "ListViewDragDrop.h"

#include "MoveListModel.h"

#include <QListView>
#include <QStringListModel>
#include <QSplitter>
#include <QDebug>

ListViewDragDrop::ListViewDragDrop(QWidget *parent)
: QMainWindow(parent)
{
QStringList listA;
listA << "String A1" << "String A2" << "String A3" << "String A4";

mpModelA = new MoveListModel( this );
connect( mpModelA, &QAbstractItemModel::dataChanged, this, &ListViewDragDrop::onModelADataChanged );
connect( mpModelA, &QAbstractItemModel::rowsInserted, this, &ListViewDragDrop::onModelARowsInserted );
connect( mpModelA, &QAbstractItemModel::rowsRemoved, this, &ListViewDragDrop::onModelARowsRemoved );
mpModelA->setStringList( listA );

QStringList listB;
listB << "String B1" << "String B2" << "String B3" << "String B4";

mpModelB = new QStringListModel( this );
connect( mpModelB, &QAbstractItemModel::dataChanged, this, &ListViewDragDrop::onModelBDataChanged );
connect( mpModelB, &QAbstractItemModel::rowsInserted, this, &ListViewDragDrop::onModelBRowsInserted );
connect( mpModelB, &QAbstractItemModel::rowsRemoved, this, &ListViewDragDrop::onModelBRowsRemoved );
mpModelB->setStringList( listB );

QSplitter * pSplitter = new QSplitter( Qt::Horizontal );

mpViewA = new QListView();
mpViewA->setModel( mpModelA );
mpViewA->setDragEnabled( true );
mpViewA->setAcceptDrops( true );

mpViewB = new QListView();
mpViewB->setModel( mpModelB );
mpViewB->setDragEnabled( true );
mpViewB->setAcceptDrops( true );

pSplitter->addWidget( mpViewA );
pSplitter->addWidget( mpViewB );
setCentralWidget( pSplitter );

}

ListViewDragDrop::~ListViewDragDrop()
{
}

void ListViewDragDrop::onModelADataChanged( const QModelIndex & topLeft, const QModelIndex & bottomRight )
{
dumpStringList( "onModelADataChanged", "Model A", mpModelA );
}

void ListViewDragDrop::onModelARowsInserted( const QModelIndex & parent, int first, int last )
{
dumpStringList( "onModelARowsInserted", "Model A", mpModelA );
}

void ListViewDragDrop::onModelARowsRemoved( const QModelIndex & parent, int first, int last )
{
dumpStringList( "onModelARowsRemoved", "Model A", mpModelA );
}

void ListViewDragDrop::onModelBDataChanged( const QModelIndex & topLeft, const QModelIndex & bottomRight )
{
dumpStringList( "onModelBDataChanged", "Model B", mpModelB );
}

void ListViewDragDrop::onModelBRowsInserted( const QModelIndex & parent, int first, int last )
{
dumpStringList( "onModelBRowsInserted", "Model B", mpModelB );
}

void ListViewDragDrop::onModelBRowsRemoved( const QModelIndex & parent, int first, int last )
{
dumpStringList( "onModelBRowsRemoved", "Model B", mpModelB );
}

void ListViewDragDrop::dumpStringList( const QString & where, const QString & modelName, QStringListModel * pModel )
{
if ( pModel )
qDebug() << "From " << where << " - Current string list for " << modelName << ":" << pModel->stringList();
}

Run this code in the debugger.

No debug output for the initial setStringList calls. But when I added slots for the modelReset() signals, I get this on startup:



From "onModelAReset" - Current string list for "Model A" : ("String A1", "String A2", "String A3", "String A4")
From "onModelBReset" - Current string list for "Model B" : ("String B1", "String B2", "String B3", "String B4")


if I click and drag "String A4" from model A to model B (making a copy of the item from A), I see this output:



From "onModelBRowsInserted" - Current string list for "Model B" : ("String B1", "String B2", "String B3", "String B4", "")
From "onModelBDataChanged" - Current string list for "Model B" : ("String B1", "String B2", "String B3", "String B4", "String A4")


Likewise, if I CLICK-DRAG "String B4" from model B to model A (again making a copy), I see this:



From "onModelARowsInserted" - Current string list for "Model A" : ("String A1", "String A2", "String A3", "String A4", "")
From "onModelADataChanged" - Current string list for "Model A" : ("String A1", "String A2", "String A3", "String A4", "String B4")


If I SHIFT-CLICK-DRAG "String A1" from model A to model B (moving it), I see this output:


From "onModelBRowsInserted" - Current string list for "Model B" : ("String B1", "String B2", "String B3", "String B4", "String A4", "")
From "onModelBDataChanged" - Current string list for "Model B" : ("String B1", "String B2", "String B3", "String B4", "String A4", "String A1")
From "onModelARowsRemoved" - Current string list for "Model A" : ("String A2", "String A3", "String A4", "String B4")


So, it looks like the following is happening:

- rowsInserted is called when the item is dropped, and the strin list shows that there is an entry added to the model, but it is not complete
- dataChanged is called after rowsInserted, and now the model is complete
- rowsRemoved is called when an item is moved out of the source model, but dataChanged is not called for that model

This makes sense, because the dataChanged signal contains indexes for items that are presently in the model. If you remove one, it no longer has a valid index in the model.

buster
16th August 2020, 20:53
So, it looks like the following is happening:

- rowsInserted is called when the item is dropped, and the strin list shows that there is an entry added to the model, but it is not complete
- dataChanged is called after rowsInserted, and now the model is complete
- rowsRemoved is called when an item is moved out of the source model, but dataChanged is not called for that model

This makes sense, because the dataChanged signal contains indexes for items that are presently in the model. If you remove one, it no longer has a valid index in the model.

So that's the way it works. Yes, it does make sense now when you point it out. I do not have enough experience with Qt to be able to read this behaviour out of the docs, so you help is greatly appreciated. When I now connect to "rowsRemoved" the drag and drop behaves the way I would expect, as I now read the correct data from the models. It is necessary though to connect "rowsRemoved" from both models, in order to cater for row reordering within a single listview.

buster
20th August 2020, 01:33
I thought I had fixed this, but discovered that if I drag an item (string) from listA and drop it over an existing item in listB then the existing item is overwritten. From the docs it seems that this behaviour can be prevented by calling setDragDropOverwriteMode(false), but that doesn't change anything. What is the correct way to prevent overwriting an existing item?

d_stranz
20th August 2020, 17:14
According to the QAbstractItemView docs:



dragDropOverwriteMode : bool

This property holds the view's drag and drop behavior

If its value is true, the selected data will overwrite the existing item data when dropped, while moving the data will clear the item. If its value is false, the selected data will be inserted as a new item when the data is dropped. When the data is moved, the item is removed as well.

The default value is false, as in the QListView and QTreeView subclasses. In the QTableView subclass, on the other hand, the property has been set to true.

Note: This is not intended to prevent overwriting of items. The model's implementation of flags() should do that by not returning Qt::ItemIsDropEnabled.


The last sentence (Note) is the important one. If you have already derived from QStringListModel to implement supportedDropActions(), then you will also need to implement the flags() method in your model class to mask out the ItemIsDropEnabled flag. ( return QStringListModel::flags( index ) & ~Qt::ItemIsDropEnabled; ) [Edit: added "index" argument to superclass call]

This doesn't make a lot of sense to me. It sounds like dragDropOverwriteMode applies to the entire model when something is dropped, not individual items, but if the default is true for QTableView, who would want behavior when dropping something erased the entire table?

buster
20th August 2020, 21:30
According to the QAbstractItemView docs:



The last sentence (Note) is the important one. If you have already derived from QStringListModel to implement supportedDropActions(), then you will also need to implement the flags() method in your model class to mask out the ItemIsDropEnabled flag. ( return QStringListModel::flags() & ~Qt::ItemIsDropEnabled; )

This doesn't make a lot of sense to me. It sounds like dragDropOverwriteMode applies to the entire model when something is dropped, not individual items, but if the default is true for QTableView, who would want behavior when dropping something erased the entire table?
Do you mean?


virtual Qt::ItemFlags flags(const QModelIndex &index) const override
{
return QStringListModel::flags() | ~Qt::ItemIsDropEnabled;
}

Probably not, as it doesn't compile.

d_stranz
20th August 2020, 22:54
No, you should be returning the logical AND of whatever flags there already are (QStringListModel:: flags()) with the bitwise NOT of Qt:: ItemIsDropEnabled. My original post had a logical OR, and I realized the mistake and fixed it a bit later.



return QStringListModel::flags( index ) & ~Qt::ItemIsDropEnabled;


Your code doesn't compile because you left out the "index" argument to the superclass call. In this case, I also left out the argument, but seriously, you have to look at what the compiler is telling you and figure out what is wrong.

buster
25th August 2020, 03:58
No, you should be returning the logical AND of whatever flags there already are (QStringListModel:: flags()) with the bitwise NOT of Qt:: ItemIsDropEnabled.


return QStringListModel::flags( index ) & ~Qt::ItemIsDropEnabled;

Yes, you are right. However, this prevents all drop actions in the view. The solution to the problem, which can be found here (https://stackoverflow.com/questions/21821106/stop-qlistview-from-deleting-entries-on-drag-drop), is to AND in Qt::ItemIsDropEnabled only if (index.isValid()). So now the problem is fixed.