[英]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.