Trying to implement drag and drop for QAbstractItemModel

Maurizio Berti maurizio.berti at gmail.com
Mon Apr 19 04:33:55 BST 2021


An important thing to remember is that you can't always completely rely on
IDEs for debugging: not only they will probably hide the actual exception
depending on their implementations, but they normally don't show the full
traceback. When in doubt, just run your program from terminal/prompt and
check the output.

In your case, the crash is caused by the fact that you didn't correctly
implement setData(), which **always** expects a bool as the returned value.

Then, the problem is that you didn't implement the most important function
for drop events in a model: dropMimeData().

Drag and drop always happens (or should happen) through QMimeData in Qt
models. Qt views (and their models) use the
"application/x-qabstractitemmodeldatalist" format to serialize data, which
is created by calling QAbstractItemModel.mimeData (see
https://doc.qt.io/qt-5/qabstractitemmodel.html#mimeData), and the structure
is the following:

- for each item:
    - write row number
    - write column number
    - write QMap (which in PyQt means the number of key/value pairs): for
each key/value pairs in mapping:
        - write role (integer)
        - write value (QVariant, aka a serialization of a possibly python
object)

Here is a possible implementation:

    def dropMimeData(self, data, action, row, column, parent):
        decoded = data.data('application/x-qabstractitemmodeldatalist')
        stream = QDataStream(decoded, QIODevice.ReadOnly)
        items = []
        startRow = startCol = 65536
        if parent.isValid():
            row = parent.row()
            column = parent.column()
        while not stream.atEnd():
            itemRow = stream.readInt()
            itemCol = stream.readInt()
            fieldCount = stream.readInt()
            display = None
            displayValid = True
            while fieldCount:
                role = stream.readInt()
                value = stream.readQVariant()
                if role == Qt.DisplayRole:
                    displayValid = True
                    display = value
                fieldCount -= 1
            if displayValid and itemRow < self.rowCount() and itemCol <
self.columnCount():
                startRow = min(startRow, itemRow)
                startCol = min(startCol, itemCol)
                items.append((itemRow, itemCol, display))
        if not items:
            return False

        minRow = minCol = 65536
        maxRow = maxCol = 0
        for itemRow, itemCol, value in items:
            targetRow = row + (itemRow - startRow)
            targetCol = column + (itemCol - startCol)
            minRow = min(targetRow, minRow)
            maxRow = max(targetRow, maxRow)
            minCol = min(targetCol, minCol)
            maxCol = max(targetCol, maxCol)
            self.dataList[targetRow][targetCol] = value
        self.dataChanged.emit(
            self.index(minRow, minCol),
            self.index(maxRow, maxCol),
        )
        return True

Some important notes:

- the dropMimeData parent argument refers to the item highlighted by the
drop indicator; this means that when you directly drop on an item (not
above/below it) you have to get the row and column coordinates of the
parent (that's the reason behind the first if statement in my code); for
basic 2d models the above replacement of row/column is fine, for tree
models it requires more attention;
- the above implementation is very basic: it only works intuitively for
single items or *close* selections (selected items that are in the
next/previous column or row of the others); you have to find your preferred
implementation based on your needs; also, it just replaces the data, so
there's no clearing for MoveAction, nor "switching" between items;
- reading through the *whole* stream is mandatory (even for roles you're
not interested into), since reading is sequential; if you don't read all
fields, you'll get unexpected behavior; remember that python evaluates
objects recursively, so you can't do something like
`fields[stream.readInt()] = stream.readQVariant()`, since the role comes
first in the stream;
- the selection order is always respected in the QDataStream; if you use
extended selection, select item(1, 1) and then item(0, 0), you'll get a
datastream with that same order; note that this also means that there is no
way (from the model) to know the item on which the drag has been started;
- in order to provide more advanced drag&drop features (such as providing
correct item replacement and/or switching for complex selections, see the
previous point), you also need to reimplement the view's startDrag() so
that the order of item respects the item on which the drag was started on
by modifying the order of selected indexes returned by the selection model;
- both rowCount and columnCount index argument should be optional (aka
keyworded: for table models it's fine to use None, for tree structures
QModelIndex() is usually preferred);
- the above respects the default implementation for both views and models;
obviously, as long as d&d is kept within the same view, nothing stops you
to implement your own mimeData() in order to provide simpler data content
(for instance, just row/column pairs) with a custom mime format and then
properly react to it in dropMimeData();

Cheers,
Maurizio

Il giorno lun 19 apr 2021 alle ore 02:15 Rodrigo de Salvo Braz <
rodrigobraz at gmail.com> ha scritto:

> Hi,
>
> I want to use drag and drop for a QAbstractItemModel. I followed the
> instructions at Qt's documentation
> <https://doc.qt.io/archives/qt-5.5/model-view-programming.html#using-drag-and-drop-with-item-views>
> quite carefully, but when I try a drag-and-drop, the program crashes.
>
> I'm using PyQt 5.9.2 / Qt 5.9.7 installed with conda, on Windows 10.
>
> Any idea what I am doing wrong here? Thanks.
>
> import sys
>
> from PyQt5 import QtCore, QtWidgets
> from PyQt5.QtCore import Qt
> from PyQt5.QtWidgets import QApplication, QMainWindow, QTableView, QAbstractItemView
>
> # Attempting to implement drag-and-drop for an QAbstractTableModel as described in
> # https://doc.qt.io/archives/qt-5.5/model-view-programming.html#using-drag-and-drop-with-item-views
>
> class TableModel(QtCore.QAbstractTableModel):
>     def __init__(self):
>         super().__init__()
>         self.dataList = [["Lion", "Tiger", "Bear"], ["Gazelle", "Ox", "Pig"], ["Mouse", "Cat", "Dog"]]
>
>     def data(self, index, role=None):
>         if role == Qt.DisplayRole:
>             return f"{self.dataList[index.row()][index.column()]}"
>
>     def setData(self, index, value, role):
>         if role == Qt.EditRole:
>             self.dataList[index.row()][index.column()] = value
>             return True
>
>     def rowCount(self, index):
>         return len(self.dataList)
>
>     def columnCount(self, index):
>         return 3
>
>     def flags(self, index):
>         return Qt.ItemIsSelectable | Qt.ItemIsEditable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled
>
>     def supportedDropActions(self):
>         return Qt.CopyAction | Qt.MoveAction
>
>     def insertRows(self, row: int, count: int, parent) -> bool:
>         self.beginInsertRows(parent, row, row + count)
>         self.dataList[row : row + count] = ["", "", ""] * count
>         self.endInsertRows()
>         return True
>
>     def removeRows(self, row: int, count: int, parent) -> bool:
>         self.beginRemoveRows(parent, row, row + count)
>         del self.dataList[row : row + count]
>         self.endRemoveRows()
>         return True
>
>
> class MainWindow(QMainWindow):
>     def __init__(self):
>         super().__init__()
>
>         self.setWindowTitle("A drag-and-drop table app")
>
>         self.tableView = QTableView()
>
>         self.model = TableModel()
>         self.tableView.setModel(self.model)
>         self.tableView.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)
>
>         self.tableView.setSelectionMode(QAbstractItemView.ExtendedSelection)
>         self.tableView.setDragEnabled(True)
>         self.tableView.viewport().setAcceptDrops(True)
>         self.tableView.setDropIndicatorShown(True)
>         self.tableView.setDragDropMode(QAbstractItemView.InternalMove)
>
>         self.setCentralWidget(self.tableView)
>
>
> app = QApplication(sys.argv)
> w = MainWindow()
> w.show()
> app.exec_()
>
>
>

-- 
È difficile avere una convinzione precisa quando si parla delle ragioni del
cuore. - "Sostiene Pereira", Antonio Tabucchi
http://www.jidesk.net
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://www.riverbankcomputing.com/pipermail/pyqt/attachments/20210419/fdf5b12c/attachment-0001.htm>


More information about the PyQt mailing list