I'm trying to set up internal drag and drop in a QListView backed by a subclassed QAbstractListModel, and it's not completely working due to some strange behaviour that I don't know how to deal with.
The drag and drop seems to go well, with the underlying Model's list being reordered, but now the ListView's indices are shifted and don't match the underlying model. For example, say the list starts as [A, B, C, D, E]. I move A to the spot between D and E. Now, in the model, the list is [B, C, D, A, E] and A is index 3. In the list, I see the same thing, but debugging shows that the ModelIndex reported by the ListView for A has row 0. So it's like the View just switched around the ModelIndices while the Model actually reordered the data. Now nothing matches up at all.
(EDIT: on making my minimal example, I discovered that the indexes here don't match up even before moving items. Instead of populating the edit form with the elements of the newly selected item, it just shows the last selected item.)
I haven't seen anything in the examples I'm using to implement this to indicate why this might be happening. Does anyone have any insight into what I might be doing wrong?
Here's an example of my code (edited to be minimal but reproduceable):
[ROOT]/ui/example.py
import os
import pickle
from typing import List, Any, Iterable
from PyQt5 import uic
from PyQt5 import QtWidgets
from PyQt5.QtCore import QAbstractListModel, Qt, QModelIndex, QVariant, \
QMimeData
from definitions import ROOT_DIR
class Thing:
def __init__(self,
name: str,
number: int):
self._name = name
self._number = number
@property
def name(self):
return self._name
@property
def number(self):
return self._number
def __str__(self):
return f"{self.name} - {self.number}"
class ThingListModel(QAbstractListModel):
ThingRole = Qt.UserRole
Mimetype = "application/vnd.something.thing"
def __init__(self,
*args,
things: List[Thing] = None,
**kwargs):
super().__init__(*args, **kwargs)
self.things = things or []
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
return len(self.things)
def data(self, index: QModelIndex, role=Qt.DisplayRole):
if not index.isValid() or index.parent().isValid():
return QVariant()
if index.row() > len(self.things):
return None
if role == Qt.DisplayRole:
thing = self.things[index.row()]
return str(thing)
if role == Qt.EditRole:
thing = self.things[index.row()]
return thing.name, thing.number
if role == ThingListModel.ThingRole:
return self.things[index.row()]
return QVariant()
def setData(self,
index: QModelIndex,
value: Any,
role: int = Qt.EditRole) -> bool:
fail_message = "Thing model edit failed"
if not index.isValid():
return False
if role == Qt.EditRole:
if not isinstance(value, Thing):
return False
list_index = index.row()
if not 0 <= list_index < self.rowCount():
return False
self.things[list_index] = value
self.dataChanged.emit(index, index, self.roleNames())
return True
return False
def insertRows(
self, row: int, count: int,
parent: QModelIndex = None) -> bool:
if parent is None:
parent = QModelIndex()
self.beginInsertRows(QModelIndex(), row, row + count - 1)
self.things[row:row] = [Thing("", 1)] * count
self.endInsertRows()
return True
def removeRows(
self, row: int, count: int,
parent: QModelIndex = None) -> bool:
if parent is None:
parent = QModelIndex()
self.beginRemoveRows(QModelIndex(), row, row + count - 1)
del self.things[row:row + count]
self.endRemoveRows()
return True
def flags(self, index: QModelIndex) -> Qt.ItemFlags:
flags = super().flags(index)
if index.isValid():
flags |= Qt.ItemIsEditable
flags |= Qt.ItemIsDragEnabled
else:
flags |= Qt.ItemIsDropEnabled
return flags
def supportedDropActions(self) -> Qt.DropActions:
return Qt.MoveAction
def mimeTypes(self) -> List[str]:
return [self.Mimetype]
def mimeData(self, indexes: Iterable[QModelIndex]) -> 'QMimeData':
sorted_indices = sorted(
[index for index in indexes if index.isValid()],
key=lambda index: index.row())
encoded_data = pickle.dumps(
[self.data(
index, ThingListModel.ThingRole
) for index in sorted_indices])
mime_data = QMimeData()
mime_data.setData(self.Mimetype, encoded_data)
return mime_data
def dropMimeData(self, data: 'QMimeData', action: Qt.DropAction, row: int,
column: int, parent: QModelIndex) -> bool:
if action == Qt.IgnoreAction:
return True
if not data.hasFormat(self.Mimetype):
return False
if column > 0:
return False
encoded_data = data.data(self.Mimetype)
decoded_data = pickle.loads(encoded_data)
self.insertRows(row, len(decoded_data))
for i, thing in enumerate(decoded_data):
self.setData(self.index(row + i, 0), thing)
return True
class CustomWindow(QtWidgets.QMainWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
uic.loadUi(os.path.join(ROOT_DIR, "ui", "custom_window.ui"),
self, 'ui')
self.setWindowTitle("Window Title")
# Models
self._pastThings = ThingListModel(things=[
Thing("A", 1),
Thing("B", 2),
Thing("C", 3),
Thing("D", 4),
Thing("E", 5)
])
# UI
self.setup_ui()
self.setup_signals()
# UI
def setup_ui(self):
self.listView_things.setSelectionMode(
QtWidgets.QAbstractItemView.SingleSelection)
self.listView_things.setDragDropMode(
QtWidgets.QAbstractItemView.InternalMove)
self.listView_things.setDragEnabled(True)
self.listView_things.setAcceptDrops(True)
self.listView_things.setDropIndicatorShown(True)
self.listView_things.setModel(self._pastThings)
self.listView_things.setCurrentIndex(
self._pastThings.index(0))
def setup_signals(self):
self.listView_things.selectionModel().currentChanged.connect(
self.on_past_thing_list_selection_changed)
self.lineEdit_name.textEdited.connect(
self.on_past_thing_edited)
self.spinBox_number.valueChanged.connect(
self.on_past_thing_edited)
# Signal Handlers
def on_past_thing_list_selection_changed(self):
indices = self.listView_things.selectedIndexes()
name, number = self._pastThings.data(indices[0], Qt.EditRole)
self.lineEdit_name.setText(name)
self.spinBox_number.blockSignals(True)
self.spinBox_number.setValue(number)
self.spinBox_number.blockSignals(False)
def on_past_thing_edited(self):
indices: List[QModelIndex] = self.listView_things.selectedIndexes()
name = self.lineEdit_name.text()
number = self.spinBox_number.value()
self._pastGlues.edit_things(row=indices[0].row(), name=name,
number=number)
if __name__ == '__main__':
app = QtWidgets.QApplication([])
window = CustomWindow()
window.show()
app.exec_()
[ROOT]/ui/custom_window.ui
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>600</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QListView" name="listView_things"/>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLineEdit" name="lineEdit_name"/>
</item>
<item>
<widget class="QSpinBox" name="spinBox_number"/>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>22</height>
</rect>
</property>
</widget>
<widget class="QStatusBar" name="statusbar"/>
</widget>
<resources/>
<connections/>
</ui>
[ROOT]/definitions.py (place in project root)
import os
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
While rewriting my code to actually be minimal and reproducible, I ended up solving my own problem.
My old code connected the currentChanged signal from the selectionModel to a handler that checked what had been selected in the view. It seems that the selectionModel updates before the view does, so I was getting old information. Fortunately, a closer read of the signal showed that it hands out the new current index, so I can just use that.
Here's the fixed code.
def setup_signals(self):
self.listView_things.selectionModel().currentChanged.connect(
lambda current, previous:
self.on_past_thing_list_selection_changed(current))
# rest of method unchanged
# Signal Handlers
def on_past_thing_list_selection_changed(self, current: QModelIndex):
name, number = self._pastThings.data(current, Qt.EditRole)
# rest of method unchanged
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.