簡體   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