简体   繁体   English

拖放后如何获取 PyQt5 QListView 索引以匹配模型的列表索引?

[英]How to get PyQt5 QListView indices to match model's list indices after drag and drop?

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.我正在尝试在由子类 QAbstractListModel 支持的 QListView 中设置内部拖放,由于一些我不知道如何处理的奇怪行为,它不能完全工作。

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.拖放似乎对 go 很好,基础模型的列表正在重新排序,但现在 ListView 的索引已移动并且与基础 model 不匹配。 For example, say the list starts as [A, B, C, D, E].例如,假设列表以 [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.我将 A 移动到 D 和 E 之间的位置。现在,在 model 中,列表是 [B, C, D, A, E] 和 A 是索引 3。在列表中,我看到相同的东西,但调试显示ListView 为 A 报告的 ModelIndex 有第 0 行。所以这就像视图刚刚切换了 ModelIndices 而 Model 实际上重新排序了数据。 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 [根]/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 [根]/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) [ROOT]/definitions.py(放在项目根目录)

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.我的旧代码将来自 selectionModel 的 currentChanged 信号连接到一个处理程序,该处理程序检查视图中已选择的内容。 It seems that the selectionModel updates before the view does, so I was getting old information.似乎 selectionModel 在视图之前更新,所以我得到了旧信息。 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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM