[英]How to get PyQt5 QListView indices to match model's list indices after drag and drop?
我正在尝试在由子类 QAbstractListModel 支持的 QListView 中设置内部拖放,由于一些我不知道如何处理的奇怪行为,它不能完全工作。
拖放似乎对 go 很好,基础模型的列表正在重新排序,但现在 ListView 的索引已移动并且与基础 model 不匹配。 例如,假设列表以 [A, B, C, D, E] 开头。 我将 A 移动到 D 和 E 之间的位置。现在,在 model 中,列表是 [B, C, D, A, E] 和 A 是索引 3。在列表中,我看到相同的东西,但调试显示ListView 为 A 报告的 ModelIndex 有第 0 行。所以这就像视图刚刚切换了 ModelIndices 而 Model 实际上重新排序了数据。 现在什么都没有匹配。
(编辑:在制作我的最小示例时,我发现这里的索引甚至在移动项目之前都不匹配。而不是用新选择的项目的元素填充编辑表单,它只显示最后一个选择的项目。)
在我用来实现这一点的示例中,我没有看到任何东西来说明为什么会发生这种情况。 有没有人知道我可能做错了什么?
这是我的代码示例(编辑为最小但可复制):
[根]/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_()
[根]/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(放在项目根目录)
import os
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
虽然重写我的代码实际上是最小的和可重现的,但我最终解决了我自己的问题。
我的旧代码将来自 selectionModel 的 currentChanged 信号连接到一个处理程序,该处理程序检查视图中已选择的内容。 似乎 selectionModel 在视图之前更新,所以我得到了旧信息。 幸运的是,对信号的仔细阅读表明它发出了新的当前索引,所以我可以使用它。
这是固定代码。
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
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.