I (simply) want to be able to use a QTableView
s Drag&Drop mechanism to move existing rows. I found lots of sources (e.g. here, here or here) which describe some aspects of dragging, dropping, inserting etc. but I'm still struggling to make it work for my case.
Here is what the solution I'm looking for should be capable of:
- work on a 'Qt-free' data structure, e.g. a list of tuples.
- operate on the data structure. i.e. when the order of items gets modified in the view it should be modified in the data structure
- look and feel of standard drag&drop enabled lists:
- select/move whole rows
- show a drop indicator for the whole line
- Further operations like deleting/editing of cells must still be possible i.e. not be touched by the drag&drop approach
This tutorial shows a solution which is very close to what I need but it uses a QStandardItemModel
rather than QAbstractTableModel
which looks semi-optimal to me because I have to operate on a 'mirrored' data structure based on QStandardItem
which is needed by QStandardItemModel
(am I right?)
The code which represents my current progress is appended below.
Currently I see two possible approaches:
Approach 1: Implement against QAbstractTableModel
and implement all needed events/slots to modify the underlying data structure:
* pro: most generic approach
* pro: no redundant data
* con: I don't know how to get informed about a finished drag&drop
operation and what index got moved where
In the code I've appended I trace all related methods I know of and print out all arguments. Here is what I get when I drag line 2 onto line 3
dropMimeData(data: ['application/x-qabstractitemmodeldatalist'], action: 2, row: -1, col: -1, parent: '(row: 2, column: 0, valid: True)')
insertRows(row=-1, count=1, parent=(row: 2, column: 0, valid: True))
setData(index=(row: 0, column: 0, valid: True), value='^line1', role=0)
setData(index=(row: 0, column: 1, valid: True), value=1, role=0)
removeRows(row=1, count=1, parent=(row: -1, column: -1, valid: False))
This output raises the following questions for me:
- why do
moveRow
/moveRows
not get called? when would they be called? - why are
insertRow
/removeRow
not called but onlyinsertRows
/removeRows
? - what does a row index of
-1
mean? - what can I do with mime data provided in
dropMimeData
? Should I use it to copy data later?
Approach 2: Use QStandardItemModel
and modify your data in parallel to the data managed by QStandardItemModel
.
* pro: there's a working example
* contra: you manage a redundant data structure which has to be consistent
with another internally managed data structure.
* contra: didn't find out how to do that exactly neither
Here is my current approach using
QAbstractTableModel
:
from PyQt5 import QtWidgets, QtCore, QtGui
class MyModel(QtCore.QAbstractTableModel):
def __init__(self, data, parent=None, *args):
super().__init__(parent, *args)
self._data = data
def columnCount(self, parent):
return 2
def rowCount(self, parent):
return len(self._data)
def headerData(self, column: int, orientation, role: QtCore.Qt.ItemDataRole):
return (('Regex', 'Category')[column]
if role == QtCore.Qt.DisplayRole and orientation == QtCore.Qt.Horizontal
else None)
def data(self, index, role: QtCore.Qt.ItemDataRole):
if role not in {QtCore.Qt.DisplayRole, QtCore.Qt.EditRole}:
return None
print("data(index=%s, role=%r)" % (self._index2str(index), self._role2str(role)))
return (self._data[index.row()][index.column()]
if index.isValid()
and role in {QtCore.Qt.DisplayRole, QtCore.Qt.EditRole}
and index.row() < len(self._data)
else None)
def setData(self, index: QtCore.QModelIndex, value, role: QtCore.Qt.ItemDataRole):
print("setData(index=%s, value=%r, role=%r)" % (self._index2str(index), value, role))
return super().setData(index, value, role)
def flags(self, index):
return (
super().flags(index)
| QtCore.Qt.ItemIsDropEnabled
| (QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsDragEnabled)
if index.isValid() else QtCore.Qt.NoItemFlags)
def dropMimeData(self, data, action, row, col, parent: QtCore.QModelIndex):
"""Always move the entire row, and don't allow column 'shifting'"""
print("dropMimeData(data: %r, action: %r, row: %r, col: %r, parent: %r)" % (
data.formats(), action, row, col, self._index2str(parent)))
assert action == QtCore.Qt.MoveAction
return super().dropMimeData(data, action, row, 0, parent)
def supportedDragActions(self):
return QtCore.Qt.MoveAction
def supportedDropActions(self):
return QtCore.Qt.MoveAction | QtCore.Qt.CopyAction
def removeRow(self, row: int, parent=None):
print("removeRow(row=%r):" % (row))
return super().removeRow(row, parent)
def removeRows(self, row: int, count: int, parent=None):
print("removeRows(row=%r, count=%r, parent=%s)" % (row, count, self._index2str(parent)))
return super().removeRows(row, count, parent)
def insertRow(self, index, parent=None):
print("insertRow(row=%r, count=%r):" % (row, count))
return super().insertRow(row, count, parent)
def insertRows(self, row: int, count: int, parent: QtCore.QModelIndex = None):
print("insertRows(row=%r, count=%r, parent=%s)" % (row, count, self._index2str(parent)))
return super().insertRows(row, count, parent)
@staticmethod
def _index2str(index):
return "(row: %d, column: %d, valid: %r)" % (index.row(), index.column(), index.isValid())
@staticmethod
def _role2str(role: QtCore.Qt.ItemDataRole) -> str:
return "%s (%d)" % ({
QtCore.Qt.DisplayRole: "DisplayRole",
QtCore.Qt.DecorationRole: "DecorationRole",
QtCore.Qt.EditRole: "EditRole",
QtCore.Qt.ToolTipRole: "ToolTipRole",
QtCore.Qt.StatusTipRole: "StatusTipRole",
QtCore.Qt.WhatsThisRole: "WhatsThisRole",
QtCore.Qt.SizeHintRole: "SizeHintRole",
QtCore.Qt.FontRole: "FontRole",
QtCore.Qt.TextAlignmentRole: "TextAlignmentRole",
QtCore.Qt.BackgroundRole: "BackgroundRole",
#QtCore.Qt.BackgroundColorRole:
QtCore.Qt.ForegroundRole: "ForegroundRole",
#QtCore.Qt.TextColorRole
QtCore.Qt.CheckStateRole: "CheckStateRole",
QtCore.Qt.InitialSortOrderRole: "InitialSortOrderRole",
}[role], role)
class MyTableView(QtWidgets.QTableView):
class DropmarkerStyle(QtWidgets.QProxyStyle):
def drawPrimitive(self, element, option, painter, widget=None):
"""Draw a line across the entire row rather than just the column we're hovering over.
This may not always work depending on global style - for instance I think it won't
work on OSX."""
if element == self.PE_IndicatorItemViewItemDrop and not option.rect.isNull():
option_new = QtWidgets.QStyleOption(option)
option_new.rect.setLeft(0)
if widget:
option_new.rect.setRight(widget.width())
option = option_new
super().drawPrimitive(element, option, painter, widget)
def __init__(self):
super().__init__()
self.setStyle(self.DropmarkerStyle())
# only allow rows to be selected
self.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
# disallow multiple rows to be selected
self.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
self.setDragEnabled(True)
self.setDragDropMode(QtWidgets.QAbstractItemView.InternalMove)
self.setDropIndicatorShown(True) # default
self.setAcceptDrops(False) # ?
self.viewport().setAcceptDrops(True) # ?
self.setDragDropOverwriteMode(False)
class HelloWindow(QtWidgets.QMainWindow):
def __init__(self) -> None:
super().__init__()
model = MyModel([("^line0", 0),
("^line1", 1),
("^line2", 2),
("^line3", 3)])
table_view = MyTableView()
table_view.setModel(model)
table_view.verticalHeader().hide()
table_view.setShowGrid(False)
self.setCentralWidget(table_view)
def main():
app = QtWidgets.QApplication([])
window = HelloWindow()
window.show()
app.exec_()
if __name__ == "__main__":
main()