My goal is to have a
QTableWidget in which the user can drag/drop rows internally. That is, the user can drag and drop one entire row, moving it up or down in the table to a different location in between two other rows. The goal is illustrated in this figure:
What I tried, and what happens
Once I have populated a
QTableWidget with data, I set its properties as follows:
table.setDragDropMode(QtGui.QAbstractItemView.InternalMove) #select one row at a time table.setSelectionMode(QtGui.QAbstractItemView.SingleSelection) table.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
Similar code makes
QListWidget behave nicely: when you move an item internally, it is dropped between two elements of the list, and the rest of the items sort themselves out in a reasonable way, with no data overwritten (in other words, the view acts like the figure above, but it is a list).
In contrast, in a table modified with the code above, things don’t work out as planned. The following figure shows what actually happens:
In words: when row i is dropped, that row becomes blank in the table. Further, if I accidentally drop row i onto row j (instead of the space between two rows), the data from row i replaces the data in row j. That is, in that unfortunate case, in addition to row i becoming blank, row j is overwritten.
Note I also tried adding
table.setDragDropOverwriteMode(False) but it didn’t change the behavior.
A way forward?
This bug report might include a possible solution in C++: it seems they reimplemented
QTableWidget, but I am not sure how to cleanly port to Python.
- Reordering items in a QTreeWidget with Drag and Drop in PyQt
- QT: internal drag and drop of rows in QTableView, that changes order of rows in QTableModel
- qt: pyqt: QTreeView internal drag and drop almost working… dragged item disappears
- How to drag & drop rows within QTableWidget
- QListWidget drag and drop items disappearing from list on Symbian
- QTableWidget Internal Drag Drop Entire Row
This seems very bizarre default behaviour. Anyway, following the code in the bug report you linked to, I have successfully ported something to PyQt. It may, or may not be as robust as that code, but it at least seems to work for the simple test case you provide in your screenshots!
The potential issues with the below implementation are:
The currently selected row doesn’t follow the drag and drop (so if you move the third row, the third row stays selected after the move). This probably isn’t too hard to fix!
It might not work for rows with child rows. I’m not even sure if a
QTableWidgetItemcan have children, so maybe it is fine.
I haven’t tested with selecting multiple rows, but I think it should work
For some reason I didn’t have to remove the row that was being moved, despite inserting a new row into the table. This seems very odd to me. It almost appears like inserting a row anywhere but the end does not increase the
rowCount()of the table.
My implementation of
GetSelectedRowsFastis a bit different to theirs. It may not be fast, and could potentially have some bugs in it (I don’t check if the items are enabled or selectable) like they did. This would also be easy to fix I think, but is only a problem if you disable a row while it is selected and someone then performs a drag/drop. In this situation, I think the better solution might be to unselect rows as they were disabled, but it depends on what you are doing with it I guess!
If you were using this code in a production environment, you would probably want to go over it with a fine-tooth-comb and make sure everything made sense. There are quite probably issues with my PyQt port, and possibly issues with the original c++ algorithm my port was based on. It does however serve as a proof that what you want can be achieved using a
Update: note there is an additional answer below for PyQt5 that also fixes some of the concerns I had above. You might want to check it out!
import sys, os from PyQt4.QtCore import * from PyQt4.QtGui import * class TableWidgetDragRows(QTableWidget): def __init__(self, *args, **kwargs): QTableWidget.__init__(self, *args, **kwargs) self.setDragEnabled(True) self.setAcceptDrops(True) self.viewport().setAcceptDrops(True) self.setDragDropOverwriteMode(False) self.setDropIndicatorShown(True) self.setSelectionMode(QAbstractItemView.SingleSelection) self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setDragDropMode(QAbstractItemView.InternalMove) def dropEvent(self, event): if event.source() == self and (event.dropAction() == Qt.MoveAction or self.dragDropMode() == QAbstractItemView.InternalMove): success, row, col, topIndex = self.dropOn(event) if success: selRows = self.getSelectedRowsFast() top = selRows # print 'top is %d'%top dropRow = row if dropRow == -1: dropRow = self.rowCount() # print 'dropRow is %d'%dropRow offset = dropRow - top # print 'offset is %d'%offset for i, row in enumerate(selRows): r = row + offset if r > self.rowCount() or r < 0: r = 0 self.insertRow(r) # print 'inserting row at %d'%r selRows = self.getSelectedRowsFast() # print 'selected rows: %s'%selRows top = selRows # print 'top is %d'%top offset = dropRow - top # print 'offset is %d'%offset for i, row in enumerate(selRows): r = row + offset if r > self.rowCount() or r < 0: r = 0 for j in range(self.columnCount()): # print 'source is (%d, %d)'%(row, j) # print 'item text: %s'%self.item(row,j).text() source = QTableWidgetItem(self.item(row, j)) # print 'dest is (%d, %d)'%(r,j) self.setItem(r, j, source) # Why does this NOT need to be here? # for row in reversed(selRows): # self.removeRow(row) event.accept() else: QTableView.dropEvent(event) def getSelectedRowsFast(self): selRows =  for item in self.selectedItems(): if item.row() not in selRows: selRows.append(item.row()) return selRows def droppingOnItself(self, event, index): dropAction = event.dropAction() if self.dragDropMode() == QAbstractItemView.InternalMove: dropAction = Qt.MoveAction if event.source() == self and event.possibleActions() & Qt.MoveAction and dropAction == Qt.MoveAction: selectedIndexes = self.selectedIndexes() child = index while child.isValid() and child != self.rootIndex(): if child in selectedIndexes: return True child = child.parent() return False def dropOn(self, event): if event.isAccepted(): return False, None, None, None index = QModelIndex() row = -1 col = -1 if self.viewport().rect().contains(event.pos()): index = self.indexAt(event.pos()) if not index.isValid() or not self.visualRect(index).contains(event.pos()): index = self.rootIndex() if self.model().supportedDropActions() & event.dropAction(): if index != self.rootIndex(): dropIndicatorPosition = self.position(event.pos(), self.visualRect(index), index) if dropIndicatorPosition == QAbstractItemView.AboveItem: row = index.row() col = index.column() # index = index.parent() elif dropIndicatorPosition == QAbstractItemView.BelowItem: row = index.row() + 1 col = index.column() # index = index.parent() else: row = index.row() col = index.column() if not self.droppingOnItself(event, index): # print 'row is %d'%row # print 'col is %d'%col return True, row, col, index return False, None, None, None def position(self, pos, rect, index): r = QAbstractItemView.OnViewport margin = 2 if pos.y() - rect.top() < margin: r = QAbstractItemView.AboveItem elif rect.bottom() - pos.y() < margin: r = QAbstractItemView.BelowItem elif rect.contains(pos, True): r = QAbstractItemView.OnItem if r == QAbstractItemView.OnItem and not (self.model().flags(index) & Qt.ItemIsDropEnabled): r = QAbstractItemView.AboveItem if pos.y() < rect.center().y() else QAbstractItemView.BelowItem return r class Window(QWidget): def __init__(self): super(Window, self).__init__() layout = QHBoxLayout() self.setLayout(layout) self.table_widget = TableWidgetDragRows() layout.addWidget(self.table_widget) # setup table widget self.table_widget.setColumnCount(2) self.table_widget.setHorizontalHeaderLabels(['Colour', 'Model']) items = [('Red', 'Toyota'), ('Blue', 'RV'), ('Green', 'Beetle')] for i, (colour, model) in enumerate(items): c = QTableWidgetItem(colour) m = QTableWidgetItem(model) self.table_widget.insertRow(self.table_widget.rowCount()) self.table_widget.setItem(i, 0, c) self.table_widget.setItem(i, 1, m) self.show() app = QApplication(sys.argv) window = Window() sys.exit(app.exec_())
Answered By – three_pineapples
Answer Checked By – Mildred Charles (BugsFixing Admin)