繁体   English   中英

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

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

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