Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

QTableView and custom model: Question about dropMimeData()



  • My custom model inherits QAbstractTableModel; the view is a QTableView. I have specified a custom MIME type which returns the value of the cell (an integer) concatenated with the row (separated by ';') from which the drag originated. The reason being that I want to only accept drags from one column to another IF they are in the same row.

    Everything but dropping seems to work, namely because my overwritten dropMimeData() function always receives row and column as -1 instead of the row and column of the item on which is being dropped.

    Do I need to derive a special class from QTableView as well to prevent this from happening? I have set the following properties for the table view in QtDesigner:

    editTriggers: NoEditTriggers
    tabKeyNavigation: yes
    showDropIndicator: yes
    dragEnabled: yes
    dragDropOverwriteMode: yes
    dragDropMode: InternalMove
    defaultDropAction: MoveAction
    ...
    selectionMode: singleSelection
    selectionBehavior: selectItems
    ... etc.
    

    Flags returned from my model are: Qt::ItemIsEnabled | Qt::ItemIsSelectable and additionally Qt::ItemIsDragEnabled or Qt::ItemIsDropEnabled depending on the column.

    I might also add that I did not re-implement canDropMimeData() because of the same problem (i.e., row and column), but it seems that perhaps it is not needed here?

    It seems difficult to put together even a very simple test program to illustrate this, but I am working on this, too. I might add that I am working with Qt version 5.11.1 which was a fairly recent stable version; would an update help here?



  • It's a little long, but here is the test case. It shows a table view with five columns and four rows. Drag and drop is enabled for the first row only, and from column 5 to column 4. However, every time the function dropMimeData() is called, the arguments row and column are set to -1 which makes it impossible to know which item is meant as drop target.

    Dragging is initiated properly, and the focus rectangle around the item in column 4 is shown as well as the drag pixmap. But when I release the mouse button, I do not receive the proper coordinates.

    Should I file a bug report? I am using Qt version 5.11.1 as noted in a previous post in this thread; OS is Linux Ubuntu 18.04.2 LTS.

    File: TestDragNDrop.pro:

    QT       += core gui widgets
    TARGET = TestDragNDrop
    TEMPLATE = app
    SOURCES += main.cpp TestModel.cpp
    HEADERS += TestModel.hpp
    

    File: TestModel.hpp:

    #ifndef TESTMODEL_HPP
    #define TESTMODEL_HPP
    
    #include <QtCore>
    #include <QAbstractTableModel>
    #include <tuple>
    
    typedef std::tuple<int,QString,int,int,int>
        ROWTYPE;
    typedef QVector<ROWTYPE>
        MODELRECS;
    
    #define MODEL_COLUMNS  5
    
    #define MODEL_ID_POS   0
    #define MODEL_TEXT_POS 1
    #define MODEL_B1_POS   2
    #define MODEL_B2_POS   3
    #define MODEL_B3_POS   4
    
    #define TEST_MIME_TYPE "application/x-mytype"
    
    class TestModel : public QAbstractTableModel
    {
      Q_OBJECT
    
    public:
      explicit TestModel(QObject *parent = nullptr);
      QVariant headerData(int section,
                          Qt::Orientation orientation,
                          int role = Qt::DisplayRole) const override;
      int rowCount(const QModelIndex &parent = QModelIndex()) const override;
      int columnCount(const QModelIndex &parent = QModelIndex()) const override;
      QVariant data(const QModelIndex &index,
                    int role = Qt::DisplayRole) const override;
      bool setData(const QModelIndex &index, const QVariant &value,
                   int role = Qt::EditRole) override;
      Qt::ItemFlags flags(const QModelIndex& index) const override;
      QMimeData *mimeData(const QModelIndexList &indexes) const override;
      bool dropMimeData(const QMimeData *mimedata,
                        Qt::DropAction action,
                        int row,
                        int column,
                        const QModelIndex & /*parent*/) override;
      QStringList mimeTypes() const override;
      Qt::DropActions supportedDragActions() const override
      { return Qt::MoveAction;  }
      Qt::DropActions supportedDropActions() const override
      { return Qt::MoveAction; }
    
    private:
      MODELRECS recs_;
    };
    
    #endif // TESTMODEL_HPP
    

    File: TestModel.cpp:

    #include "TestModel.hpp"
    
    TestModel::TestModel(QObject *parent)
      : QAbstractTableModel(parent)
    {
      recs_ = MODELRECS{
                 {1,"John Doe",1,2,100},
                 {2,"Jane Doe",3,4,200},
                 {3,"Bill Smith",5,6,300},
                 {4,"Janet Kelly",7,8,400}
              };
    }
    
    QVariant TestModel::headerData(int section,
                                   Qt::Orientation orientation,
                                   int role) const
    {
      QVariant retval;
      // show only horizontal header:
      if ((orientation == Qt::Horizontal) && (role == Qt::DisplayRole)) {
        switch (section) {
        case MODEL_ID_POS:
          retval = "ID:";
          break;
        case MODEL_TEXT_POS:
          retval = "Name:";
          break;
        case MODEL_B1_POS:
          retval = "1st choice:";
          break;
        case MODEL_B2_POS:
          retval = "2nd choice:";
          break;
        case MODEL_B3_POS:
          retval = "3rd choice:";
          break;
        }
      }
      return retval;
    }
    
    int TestModel::rowCount(const QModelIndex &parent) const
    {
      if (!parent.isValid()) {
        return recs_.size();
      }
      return 0;
    }
    
    int TestModel::columnCount(const QModelIndex &parent) const
    {
      if (!parent.isValid()) {
        if (!recs_.isEmpty())
          return MODEL_COLUMNS;
      }
      return 0;
    }
    
    QVariant TestModel::data(const QModelIndex &index, int role) const
    {
      QVariant retval;
      if (index.isValid()) {
        int row = index.row();
        int col = index.column();
        if (role == Qt::DisplayRole) {
          // Normally I would use QSqlRecord as ROWTYPE, but unfortunately
          // I chose std::tuple to avoid pulling in all the SQL headers
          // and cannot use "col" here directly because it was
          // initialized with a non-const expression:
          switch(col) {
          case MODEL_ID_POS:
            retval = std::get<MODEL_ID_POS>(recs_.at(row));
            break;
          case MODEL_TEXT_POS:
            retval = std::get<MODEL_TEXT_POS>(recs_.at(row));
            break;
          case MODEL_B1_POS:
            retval = std::get<MODEL_B1_POS>(recs_.at(row));
            break;
          case MODEL_B2_POS:
            retval = std::get<MODEL_B2_POS>(recs_.at(row));
            break;
          case MODEL_B3_POS:
            retval = std::get<MODEL_B3_POS>(recs_.at(row));
            break;
          default:
            break;
          }
        }
      }
      return retval;
    }
    
    bool TestModel::setData(const QModelIndex &index,
                            const QVariant &value,
                            int role)
    {
      if (index.isValid() && (role == Qt::EditRole)) {
        int row = index.row();
        int col = index.column();
        ROWTYPE &r = recs_[row];
        bool ok = false;
        // Normally I would use QSqlRecord as ROWTYPE, but unfortunately
        // I chose std::tuple to avoid pulling in all the SQL headers
        // and cannot use "col" here directly because it was
        // initialized with a non-const expression:
        switch(col) {
        case MODEL_ID_POS:
          std::get<MODEL_ID_POS>(r) = value.toInt(&ok);
          break;
        case MODEL_TEXT_POS:
          std::get<MODEL_TEXT_POS>(r) = value.toString();
          ok = true;
          break;
        case MODEL_B1_POS:
          std::get<MODEL_B1_POS>(r) = value.toInt(&ok);
          break;
        case MODEL_B2_POS:
          std::get<MODEL_B2_POS>(r) = value.toInt(&ok);
          break;
        case MODEL_B3_POS:
          std::get<MODEL_B3_POS>(r) = value.toInt(&ok);
          break;
        default:
          break;
        }
        if (ok) {
          emit dataChanged(index, index, QVector<int>() << role);
          return true;
        }
      }
      return false;
    }
    
    Qt::ItemFlags TestModel::flags(const QModelIndex &index) const
    {
      if (!index.isValid())
        return Qt::NoItemFlags;
    
      Qt::ItemFlags flags = Qt::ItemIsEnabled | Qt::ItemIsSelectable;
    
      int row = index.row();
      int col = index.column();
    
      // Only allow DnD in the first row:
      if ((row == 0) && (col == MODEL_B3_POS))
        flags |= Qt::ItemIsDragEnabled;
      else if ((row == 0) && (col == MODEL_B2_POS))
        flags |= Qt::ItemIsDropEnabled;
    
      return flags;
    }
    
    QMimeData *TestModel::mimeData(const QModelIndexList &indexes) const
    {
      QMimeData *pMimeData = nullptr;
    
      if (!indexes.isEmpty()) {
        QModelIndex idx = indexes.at(0);
        // Only allow DnD in the first row:
        if (idx.row() == 0) {
          QVariant value = data(idx);
          if (value.isValid()) {
            QString mimeText = value.toString() + ";0";
            pMimeData = new QMimeData;
            if (pMimeData) pMimeData->setData(TEST_MIME_TYPE, mimeText.toUtf8());
          }
        }
      }
      return pMimeData;
    }
    
    QStringList TestModel::mimeTypes() const
    {
      return QStringList() << TEST_MIME_TYPE;
    }
    
    bool TestModel::dropMimeData(const QMimeData *mimedata,
                                 Qt::DropAction action,
                                 int row,
                                 int column,
                                 const QModelIndex & /* parent is not used */)
    {
      if (!mimedata) return false;
      if (action != Qt::MoveAction) return false;
      if ((row < 0) || (column < 0)) return false;
      if (!mimedata->hasFormat(TEST_MIME_TYPE)) return false;
    
      QString dta = QString::fromUtf8(mimedata->data(TEST_MIME_TYPE));
      QStringList sl = dta.split(';');
      Q_ASSERT(sl.size() == 2);
      bool ok = false;
      QString sval, srow;
      sval = sl.at(0);
      srow = sl.at(1);
      int r = -1;
      int v = sval.toInt(&ok);
      if (ok) {
        r = srow.toInt(&ok);
      }
      if (!ok) return false;
      if (r == row) {
        // accept the drop:
        return setData(index(row,column), v);
      }
      return false;
    }
    

    File: main.cpp:

    #include <QApplication>
    #include <QTableView>
    #include "TestModel.hpp"
    
    int main(int argc, char *argv[])
    {
      QApplication a(argc, argv);
      QTableView view;
      TestModel model;
    
      view.setDefaultDropAction(Qt::MoveAction);
      view.setDragDropMode(QAbstractItemView::InternalMove);
      view.setDragEnabled(true);
      view.setEditTriggers(QAbstractItemView::NoEditTriggers);
      view.setSelectionBehavior(QAbstractItemView::SelectItems);
      view.setSelectionMode(QAbstractItemView::SingleSelection);
      view.showDropIndicator();
      view.setModel(&model);
      view.setMinimumWidth(600);
      view.setMinimumHeight(100);
      view.show();
    
      return a.exec();
    }
    


  • Even after simplifying the logic to allow drops from column 5 into column 4 in ANY row of the table, none of the drops work ... the dropMimeData() function still receives row == -1 and column == -1 although the drag symbol changes to the drop symbol as it should when the mouse is over any cell in the 4th column.


  • Qt Champions 2019

    You're ignoring the parent, see documentation: "When a drop occurs, the model index corresponding to the parent item will either be valid, indicating that the drop occurred on an item, or it will be invalid, indicating that the drop occurred somewhere in the view that corresponds to top level of the model."



  • @christian-ehrlicher : Does this mean that I should get the row and column number from the index and not from the arguments passed to the function? In a table model, the parent is almost always an invalid QModelIndex in rowCount() and similar functions.

    (EDIT: I just tested this, and now it seems to work... Thank you very much!)


Log in to reply